内核驱动编写的简要框架
设备驱动大致骨架
驱动代码.c文件
#include<linux/module.h>
#include<linux/init.h>
#include <linux/slab.h>//使用kmalloc就需要include
static unsigned char men[100000];
//设备文件file,buf数据位置,count可读字节数,ppos偏移量
static ssize_t 驱动读函数(struct file *file,char *buf,size_t count,loff_t *ppos){
copy_to_user(buf,(void*)mem,written_count);
return written_count;
}
//设备文件file,buf数据,count可写字节数,ppos偏移量
static ssize_t 驱动写函数(struct file *file,char *buf,size_t count,loff_t *ppos){
copy_from_user(mem,buf,count);
return count;
}
#define DEVICE_NAME "驱动名字"
static struct file_operations dev_fops =
{
.owner=THIS_MODULE,
.read=驱动读函数,
.write=驱动写函数,
……其他操作比如poll,ioctl
} ;
static struct miscdecice misc=
{
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &dev_fops
};
int _init 初始化驱动函数()//_init是个编译命令,让编译器知道这个是驱动初始化函数,从而将其放到某个内存区域,优化运行,非必须
{
printk(打印内核日志信息);
//atoi(用户空间) simple_strtol(内核空间)
//itoa(用户空间) snprintf(内核空间)
int ret=misc_register(&misc);//注册设备文件,注册完后,dev/目录下会多个驱动文件,read/write这个文件就能调用你的驱动read/write了。如果成功注册了设备文件,misc_register函数返回非0的整数,如果注册设备文件失败,返回0。
}
int _exit 卸载驱动函数()//_exit是个编译命令,让编译器知道这个是驱动卸载函数,从而将其放到某个内存区域,优化运行,非必须
{
misc_deregister()//注销设备文件
}
MODULE_AUTHOR(作者) //可选
MODULE_LICENSE(开源协议) //可选
MODULE_ALIAS(别名) //可选
MODULE_DESCRIPTION(描述) //可选
module_init(初始化驱动函数)
module_exit(卸载驱动函数)
同级目录下Makefile文件
下面3选1,对应不同的项目结构
obj-m+=驱动名字.o#需要手动insmod(单纯安装)、modprobe(检测依赖后进行安装) 和rmmod
obj-m+=驱动名字.o#系统启动时,它会自动使用modprobe安装驱动,不需要你手动执行安装驱动的命令。
obj-$(CONFIG_驱动名字)+=驱动名字.o#根据mk配置项来决定,一般还要在某个defconfig文件配置CONFIG_驱动名字的值
编程理念
- static
因为在C语言中用static声明函数、变量等资源,系统会将这些函数和变量单独放在内存的某一个区域,直到程序完全退出,否则这些资源不会被释放。Linux驱动一旦装载,除非手动卸载或关机,驱动会一直驻留内存,因此这些函数和变量资源会一直在内存中。也就是说多次调用这些资源不用再进行压栈、出栈操作了,有利于提高驱动的运行效率。
- 避免使用浮点
- 不可使用c库
- 目前搜寻的资料中表示,驱动的阻塞与否完全依赖于自身代码的实现,内核并不干预这个。所以如果用户使用非阻塞打开,而代码里不管fd的O_NONBLOCK,一律阻塞的话。也是可行的。这个我还不是很确定,有点空试一下。
- 模块依赖
- Linux驱动并不直接向硬件中的内存写数据,而是与本机的I/O内存(I/O Memory,位于内核空间)进行交互。所谓I/O内存是通过各种接口(PCI、USB、蓝牙、以太网口等)连接到主机(PC、手机)的硬件(网卡、声卡、摄像头等)在主机内存中的映射。例如,在Ubuntu Linux上运行的驱动只需要访问运行Ubuntu Linux的主机中的I/O内存即可,然后Linux内核会利用I/O内存中的数据硬件交互。Linux内核提供了多个与I/O内存交互的函数,如ioread16、ioread32、iowrite16、iowrite32等。Linux内核的内存管理模块负责同步I/O内存与硬件中的数据。每一个连接Linux的硬件在I/O内存中都会有映射首地址。在使用ioread32、ioread32等函数读写I/O内存时需要指定这些首地址。开发板上的LED也有其映射首地址。
- misc_register函数只能设置次设备号。主设备号统一设为10。主设备号为10的设备是Linux系统中拥有共同特性的简单字符设备。这类设备称为misc设备。
调试
- ls -l /dev
- dmesg 查看内核输出
- echo 输入 > /dev/你的驱动
- cat /dev/你的驱动
更灵活的创建设备文件的方式
misc_register函数使用简单,但却不灵活。misc_register函数只能建立主设备号为10的设备文件。如果想建立其他主设备号的设备文件,就需要使用cdev_init、register_chrdev_region、cdev_add、class_create、device_create等函数
模板如下
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#define DEVICE_NAME "mychardev"
#define CLASS_NAME "mycharclass"
static dev_t dev_num; // 设备号
static struct class *dev_class = NULL; // 设备类
static struct cdev my_cdev; // 字符设备结构体
// 设备文件操作函数的声明
static int 设备打开函数(struct inode *inode, struct file *file);
static int 设备释放函数(struct inode *inode, struct file *file);
static ssize_t 设备读函数(struct file *file, char __user *user_buffer, size_t len, loff_t *offset);
static ssize_t 设备写函数(struct file *file, const char __user *user_buffer, size_t len, loff_t *offset);
// 设备文件操作结构体
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = 设备打开函数,
.release = 设备释放函数,
.read = 设备读函数,
.write = 设备写函数,
};
static int 设备打开函数(struct inode *inode, struct file *file) {
略
return XX;
}
static int 设备释放函数(struct inode *inode, struct file *file) {
略
return XX;
}
static ssize_t 设备读函数(struct file *file, char __user *user_buffer, size_t len, loff_t *offset) {
略
return XX;
}
// 写入设备的函数
static ssize_t 设备写函数(struct file *file, const char __user *user_buffer, size_t len, loff_t *offset) {
略
return XX;
}
// 模块初始化函数
//_init是个编译命令,让编译器知道这个是驱动初始化函数,从而将其放到某个内存区域,优化运行,非必须
static int __init mychar_init(void) {
int ret;
// 动态分配设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
printk(KERN_ALERT "Failed to allocate device number\n");
return ret;
}
// 初始化字符设备
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
// 添加字符设备到系统
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret < 0) {
unregister_chrdev_region(dev_num, 1);
printk(KERN_ALERT "Failed to add cdev\n");
return ret;
}
// 创建设备类,需要udev
dev_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(dev_class)) {
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ALERT "Failed to create class\n");
return PTR_ERR(dev_class);
}
// 创建设备节点
if (IS_ERR(device_create(dev_class, NULL, dev_num, NULL, DEVICE_NAME))) {
class_destroy(dev_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ALERT "Failed to create device\n");
return PTR_ERR(dev_class);
}
printk(KERN_INFO "Device initialized successfully\n");
return 0;
}
// 模块退出函数
//_exit是个编译命令,让编译器知道这个是驱动卸载函数,从而将其放到某个内存区域,优化运行,非必须
static void __exit mychar_exit(void) {
// 删除设备节点
device_destroy(dev_class, dev_num);
// 销毁设备类
class_destroy(dev_class);
// 删除字符设备
cdev_del(&my_cdev);
// 释放设备号
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Device exited successfully\n");
}
module_init(mychar_init);
module_exit(mychar_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("驱动模板");
MODULE_VERSION("1.0");
可以使用MKDEV MAJOR MINOR处理设备号。主设备号12位,次设备号20位。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)