字符设备驱动开发
字符设备驱动开发
初识
字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动 。

在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程
序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。 open和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态
open():打开驱动文件
wirte():写入数据控制灯的亮灭
read():读取灯的状态
close():关闭驱动函数
Linux程序运行在用户空间,而Linux 驱动属于内核的一部分,因此驱动运行于内核空间。 当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做 “系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。 open、 close、 write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的时候流程如图

应用程序使用到的函数在具体驱动程序中都有与之对应的函数,
比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数
驱动开发就是实现对应的函数
Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。
而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux 内核中,当然也可以不编译进 Linux 内核中,具体看自己的需求。模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数初写
头文件
头文件路径前的 linux/ 表示这些头文件属于 Linux 内核源码,而不是标准 C 库
设备号
Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分
组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面
其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux
系统中主设备号范围为 0~4095
static
确保驱动不对外暴露接口
C99结构体
声明并且定义
static struct file_operations chrdevbase_fops = { .owner = THIS_MODULE, // 明确指定成员,无需按顺序
.open = chrdevbase_open, // 可读性强
.read = chrdevbase_read,
.write = chrdevbase_write, .release = chrdevbase_release,
};传统
// 声明(仅告诉编译器类型)
static struct file_operations chrdevbase_fops;
// 定义(运行时赋值)
chrdevbase_fops.owner = THIS_MODULE;
chrdevbase_fops.open = chrdevbase_open; chrdevbase_fops.read = chrdevbase_read; // ... 其他成员赋值file_operations里是Linux内核定义的结构体,成员是很多函数指针,我们要规定函数指针指向自定义的函数
printf与printk
在 Linux 内核中没有 printf 这个函数。 printk 相当于 printf 的孪生兄妹, printf
运行在用户态, printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用printk 这个函数。不同之处在于, printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面 ,0-7,默认是4
流程
1.模式
- 用户态调用 open、read、write、close
- 内核先交给 VFS
- VFS 再根据对象类型转给具体实现
2.数据传输
copy_to_user(buf,readbuf,cnt)copy_->复制之后内核发送给用户,也就是用户调用read()
先把内核数据kerneldata放在缓冲区readbuf里面,再把缓冲区readbuf里面的内容发送给用户(函数参数列表里面会有变量)
copy_from_user(writebuf,buf,cnt)copy_->从用户那里复制到内核,也就是用户调用write()
用户数据依然是在函数参数列表buf里拿,再放在内核缓冲区里面
module_init(xxx)//装载时会调用xxx函数
//你执行 insmod/modprobe 后,内核模块加载器会装载 .ko。
//module_init(char_device_init) 这个宏会把入口函数记录为该模块的初始化入口(等价于告诉内核“加载我时先调这个函数”)。
//内核在模块装载阶段调用这个入口,于是进入 char_device.c:76 的 char_device_init。
//在这个函数里你调用了 char_device.c:80 的 register_chrdev 完成字符设备注册
#include<linux/types.h>
#include<linux/kernel.h>
#include<linux/delay.h>
#include<linux/ide.h>
#include<linux/init.h>
#include<linux/module.h>
#define CHAR_MAJOR 200
#define CHAR_NAME "char_device"
//内核空间
static char readbuf[100];//读缓冲区
static char writebuf[100];//写缓冲
//内核数据
static char kerneldata[]="让我受尽折磨!!";
static int char_device_open(struct inode *inode,struct file *file)
{
return 0;
}
static int char_device_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 senddate ok!\r\n");
}
else{
printk("kernel senddata failed!\r\n");
}
return 0;
}
static ssize_t char_device_write(struct file *file,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 received data:%s\r\n",writebuf);
}
else
{
printk("kernel received failed!\r\n");
}
return 0;
}
static int char_device_release(struct inode *inode,struct file *file)
{
return 0;
}
static struct file_operations char_device_fops={
.owner=THIS_MODULE,
.open=char_device_open,
.read=char_device_read,
.write=char_device_write,
.release=char_device_release,
};
static int __init char_device_init(void)
{
int retvalue=0;
retvalue=register_chrdev(CHAR_MAJOR,CHAR_NAME,&char_device_fops);
if(retvalue<0)
{
printk("char device register failed\r\n");
}
printk("char_device init!\r\n");
return 0;
}
static void __exit char_device_exit(void)
{
unregister_chrdev(CHAR_MAJOR,CHAR_NAME);
printk("char_device exit!\r\n");
}
module_init(char_device_init);
module_exit(char_device_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZLX");2.编译
KERNELDIR := /home/hqyj/I.MX6ULL/linux_kernel
CURRENT_PATH := $(shell pwd)
obj-m := char_device.o # 确保文件名匹配你的 c 文件
build: kernel_modules
kernel_modules:
$(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(CURRENT_PATH) clean编译成功以后就会生成一个叫做 chrdevbaes.ko 的文件,此文件就是 chrdevbase 设备的驱动
模块。至此,chrdevbase 设备的驱动就编译成功。
3.编写用户层
再写一个操作该设备的用户层c文件就行了
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
static char usrdata[] = {"usr data!"};
int main(int argc,char *argv[])
{
int fd,retvalue;
char *filename;
char readbuf[100],writebuf[100];
if(argc!=3)
{
printf("Error usage!\r\n");
}
filename=argv[1];
fd=open(filename,O_RDWR);
if(fd<0)
{
perror("open error");
exit(-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)
{
perror("open error");
exit(-1);
}
return 0;
}编译
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp4.移动文件位置到开发板
检查开发板根文件系统中有没有“/lib/modules/4.1.15”这个目录,如果没有的话自行创建。
注意,“/lib/modules/4.1.15”这个目录用来存放驱动模块,使用modprobe 命令加载驱动模块的时候,驱动模块要存放在此目录下。
“/lib/modules”是通用的,不管你用的什么板子、什么内核,这部分是一样的。
不一样的是后面的“4.1.15”,这里要根据你所使用的 Linux 内核版本来设置,比如 ALPHA 开发板现在用的是 4.1.15 版本的 Linux 内核,因此就是“/lib/modules/4.1.15”。
如果你使用的其他版本内核,比如 5.14.31,那么就应该创建“/lib/modules/5.14.31”目录,否则 modprobe 命令无法加载驱动模块。
.ko .app
scp file root@192.168.x.x:/path/5.加载驱动文件
在/lib/modules/4.1.15……文件下
输入如下命令加载 chrdevbase.ko 驱动文件:
depmod //第一次加载驱动的时候需要运行此命令然后
insmod chrdevbase.ko
或
modprobe chrdevbase.ko输入
lsmod命令即可查看当前系统中存在的模块
输入如下命令查看当前系统中有所有的设备:
cat /proc/devices6.创建设备节点文件
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操
作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase 这个设备节点文件:
mknod /dev/chrdevbase c 200 0其中“mknod”是创建节点命令,“/dev/chrdevbase”是要创建的节点文件,“c”表示这是个
字符设备,“200”是设备的主设备号,“0”是设备的次设备号。创建完成以后就会存在
/dev/chrdevbase 这个文件,可以使用
ls /dev/chrdevbase -l命令查看
,新字符设备驱动自动创建,不用这个步骤7.操作测试
加载了,创建了/dev/驱动名 ,之后就能操作 这个文件了
读出内核
./chrdevbaseApp /dev/chrdevbase 1写入内核
./chrdevbaseApp /dev/chrdevbase 28.卸载驱动
rmmod chrdevbase.ko卸载以后使用
lsmod查看 chrdevbase 这个模块还存不存在

