内核驱动编写的简要框架

设备驱动大致骨架

驱动代码.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位。

posted @   任侠平生愿  阅读(24)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示