七、新增设备驱动

新增设备驱动

2.7.1、编写规则

在 Linux 中,设备驱动程序是内核模块,用于实现内核与硬件设备之间的通信。设备驱动主要分为字符设备驱动、块设备驱动和网络设备驱动。

  • 字符设备驱动:以字节流的形式处理数据,如串口、键盘等。
  • 块设备驱动:以块为单位处理数据,如硬盘。
  • 网络设备驱动:用于网络通信。
1、编写规范
  1. 头文件包含

    内核驱动需要包含内核特定的头文件,这些头文件提供了内核数据结构、函数和宏的定义。常见的头文件有:

    #include <linux/init.h>  // 模块初始化和退出相关
    #include <linux/module.h>  // 模块编程基本定义
    #include <linux/fs.h>  // 文件系统相关定义
    #include <linux/uaccess.h>  // 用户空间和内核空间数据交互
    
  2. 模块初始化和退出函数

    每个内核模块都需要定义初始化和退出函数。

    static int __init my_driver_init(void) {
        // 初始化代码
        return 0;
    }
    
    static void __exit my_driver_exit(void) {
        // 退出代码
    }
    
    module_init(my_driver_init);
    module_exit(my_driver_exit);
    
    • __init 标记的函数在模块加载时执行,加载完成后相关内存可能会被释放。
    • __exit 标记的函数在模块卸载时执行。
  3. 模块许可证声明

    必须声明模块的许可证,常见的是 GPL 许可证。

    MODULE_LICENSE("GPL");
    
  4. 设备注册和注销

    对于字符设备驱动,需要注册和注销字符设备。

    static int major_number;
    major_number = register_chrdev(0, "my_device", &fops);  // 注册字符设备
    unregister_chrdev(major_number, "my_device");  // 注销字符设备
    
    • register_chrdev 第一个参数为 0 表示让内核自动分配主设备号。
    • fops 是文件操作结构体。
  5. 文件操作结构体

    定义设备的读写等操作函数。

    static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
        // 读取操作代码
        return 0;
    }
    
    static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
        // 写入操作代码
        return 0;
    }
    
    static struct file_operations fops = {
        .read = my_read,
        .write = my_write,
    };
    
  6. 用户空间和内核空间数据交互

    使用 copy_to_usercopy_from_user 进行数据交互。

    if (copy_to_user(buf, kernel_buffer, count)) {
        return -EFAULT;
    }
    
    if (copy_from_user(kernel_buffer, buf, count)) {
        return -EFAULT;
    }
    
2、字符设备驱动示例
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mychardevice"
#define BUFFER_SIZE 1024

static char buffer[BUFFER_SIZE];
static int major_number;

static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    if (*f_pos >= BUFFER_SIZE)
        return 0;
    if (*f_pos + count > BUFFER_SIZE)
        count = BUFFER_SIZE - *f_pos;
    if (copy_to_user(buf, buffer + *f_pos, count))
        return -EFAULT;
    *f_pos += count;
    return count;
}

static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    if (*f_pos >= BUFFER_SIZE)
        return 0;
    if (*f_pos + count > BUFFER_SIZE)
        count = BUFFER_SIZE - *f_pos;
    if (copy_from_user(buffer + *f_pos, buf, count))
        return -EFAULT;
    *f_pos += count;
    return count;
}

static struct file_operations fops = {
    .read = my_read,
    .write = my_write,
};

static int __init my_init(void) {
    major_number = register_chrdev(0, DEVICE_NAME, &fops);
    if (major_number < 0) {
        printk(KERN_ALERT "Failed to register character device\n");
        return major_number;
    }
    printk(KERN_INFO "Character device registered with major number %d\n", major_number);
    return 0;
}

static void __exit my_exit(void) {
    unregister_chrdev(major_number, DEVICE_NAME);
    printk(KERN_INFO "Character device unregistered\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
3、创建 Makefile

为了编译驱动模块,需要创建一个 Makefile 文件。

obj-m += mychardevice.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean
4、编译驱动模块

在终端中执行以下命令来编译驱动模块:

make

这会生成一个名为 mychardevice.ko 的内核模块文件。

5、加载和卸载驱动模块
  1. 加载模块

    使用 insmod 命令加载驱动模块:

    sudo insmod mychardevice.ko
    
  2. 查询模块

    使用 lsmod 命令查看已加载的模块:

    lsmod | grep mychardevice
    
  3. 卸载模块

    使用 rmmod 命令卸载驱动模块:

    sudo rmmod mychardevice
    
6、创建设备节点

为了在用户空间访问设备驱动,需要创建一个设备节点。

sudo mknod /dev/mychardevice c <major_number> 0

<major_number> 是在驱动初始化时分配的主设备号。

7、测试驱动

编写一个简单的用户空间程序来测试驱动的读写功能。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define DEVICE_PATH "/dev/mychardevice"

int main() {
    int fd;
    char buffer[1024];

    // 打开设备文件
    fd = open(DEVICE_PATH, O_RDWR);
    if (fd < 0) {
        perror("Failed to open device file");
        return -1;
    }

    // 写入数据
    write(fd, "Hello, device!", 14);

    // 读取数据
    lseek(fd, 0, SEEK_SET);
    read(fd, buffer, 14);
    printf("Read from device: %s\n", buffer);

    // 关闭设备文件
    close(fd);

    return 0;
}

编译并运行这个程序:

gcc -o test test.c
./test
9、注意事项
  • 内存管理:在内核中,内存管理非常重要。避免内存泄漏,确保在模块退出时释放所有分配的内存。
  • 并发和同步:内核是多任务的,需要考虑并发访问的问题。使用适当的同步机制,如互斥锁、信号量等。
  • 错误处理:在代码中添加充分的错误处理,确保在出现错误时能正确处理,避免内核崩溃。
  • 许可证兼容性:确保使用的代码和库与 GPL 许可证兼容。
  • 调试:使用 printk 函数输出调试信息,使用 dmesg 命令查看内核日志。
2.7.2、新增IIC驱动

着重点:

设备树和驱动匹配: 设备树中的 compatible = "myvendor,i2c_dev" 字段指示该设备是一个 I2C 类型的 i2c_dev 设备。驱动程序通过设备树匹配表(of_device_id)中的相应条目来查找是否存在名为 "myvendor,i2c_dev" 的设备。如果存在,驱动程序就会被加载并执行相应的初始化(即 probe 函数)。

设备 ID 表gtp_device_id 表与设备树中的设备信息相对应。它通过 i2c_device_id 中的 ID 名称(例如 "myvendor,i2c_dev")来指定哪些设备应该由当前驱动程序管理。实际上,它与设备树中的 compatible 字段是相对应的。

驱动程序初始化: 当系统识别到一个 compatible 字段匹配的设备时,内核会调用驱动的 probe 函数进行初始化。在 probe 函数中,驱动可以读取设备树中的其他信息(例如 reg 地址),并进行相应的设备初始化。

1、设备树

./rk3566-evb2-lp4x-v10.dtsi

&i2c2 {
	status = "okay";
	pinctrl-0 = <&i2c2m1_xfer>;

	dev_device: device@50 {
        compatible = "myvendor,i2c_dev";
        status = "okay";
        reg = <0x18>;
    };
2、驱动

./kernel/drivers/i2c/busses/user_dev.h

#ifndef __USER_dev_H__
#define __USER_dev_H__

#define I2C_ADDR_START      0x10
#define I2C_ADDR_END        0x20

#endif

./kernel/drivers/i2c/busses/user_dev.c
iic写数据

static ssize_t dev_write_data(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {
    unsigned char reg_type;
    unsigned char *kernel_buf = NULL;
    int ret;

    if (count < 1) {
        pr_err("dev_write_data error: insufficient data provided\n");
        return -EINVAL;
    }

    if (count > 1) {
        kernel_buf = kmalloc(count - 1, GFP_KERNEL);
        if (!kernel_buf) {
            pr_err("dev_write_data error: memory allocation failed\n");
            return -ENOMEM;
        }

        if (copy_from_user(kernel_buf, buf + 1, count - 1)) {
            pr_err("dev_write_data error: failed to copy data\n");
            kfree(kernel_buf);
            return -EFAULT;
        }
    }

    if (copy_from_user(&reg_type, buf, 1)) {
        pr_err("dev_write_data error: failed to copy reg_type\n");
        kfree(kernel_buf);
        return -EFAULT;
    }

    pr_info("dev_write_data: reg_type=0x%02x, data_length=%zu\n", reg_type, count - 1);

    ret = i2c_write_dev(dev_client, reg_type, kernel_buf, count - 1);
    kfree(kernel_buf);

    if (ret < 0) {
        pr_err("dev_write_data error: i2c_write_dev failed\n");
        return ret;
    }

    return count;
}

iic读数据

static ssize_t dev_read_data(struct file *file, char __user *user_buf, size_t count, loff_t *ppos) {
    u8 *data_buffer;
    int ret;

    // 验证 count 参数是否有效
    if (count < 1) {
        pr_err("dev_read_data: insufficient count (count=%zu)\n", count);
        return -EINVAL;
    }

    // 分配缓冲区
    data_buffer = kmalloc(count, GFP_KERNEL);
    if (!data_buffer) {
        pr_err("dev_read_data: memory allocation failed\n");
        return -ENOMEM;
    }

    // 调用 i2c_read_dev 读取数据
    ret = i2c_read_dev(dev_client, data_buffer, count);
    if (ret < 0) {
        pr_err("dev_read_data: i2c_read_dev failed (ret=%d)\n", ret);
        kfree(data_buffer);
        return ret;
    }

    // 将数据复制到用户空间
    if (copy_to_user(user_buf, data_buffer, count)) {
        pr_err("dev_read_data: failed to copy data to user\n");
        kfree(data_buffer);
        return -EFAULT;
    }

    // 释放缓冲区并返回读取的字节数
    kfree(data_buffer);
    return count;
}

probe

static int dev_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret = -1;

    pr_info("my I2C Device dev Probed\n");

    // 注册字符设备部分
    ret = alloc_chrdev_region(&dev_devno, 0, DEV_CNT, DEV_NAME);
    if (ret < 0) {
        printk(KERN_ERR "Failed to allocate dev_devno, error: %d\n", ret);
        return ret;
    }

    // 初始化字符设备结构体
    cdev_init(&dev_chr_dev, &dev_chr_dev_fops);
    dev_chr_dev.owner = THIS_MODULE;

    // 添加设备至 cdev_map 散列表中
    ret = cdev_add(&dev_chr_dev, dev_devno, DEV_CNT);
    if (ret < 0) {
        printk(KERN_ERR "Failed to add cdev, error: %d\n", ret);
        unregister_chrdev_region(dev_devno, DEV_CNT); // 清理已分配的设备号
        return ret;
    }

    // 创建类
    class_dev = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(class_dev)) {
        ret = PTR_ERR(class_dev);
        printk(KERN_ERR "Failed to create class, error: %d\n", ret);
        cdev_del(&dev_chr_dev); // 清理已添加的 cdev
        unregister_chrdev_region(dev_devno, DEV_CNT); // 清理已分配的设备号
        return ret;
    }

    // 创建设备
    device_dev = device_create(class_dev, NULL, dev_devno, NULL, DEV_NAME);
    if (IS_ERR(device_dev)) {
        ret = PTR_ERR(device_dev);
        printk(KERN_ERR "Failed to create device, error: %d\n", ret);
        class_destroy(class_dev); // 清理已创建的类
        cdev_del(&dev_chr_dev); // 清理已添加的 cdev
        unregister_chrdev_region(dev_devno, DEV_CNT); // 清理已分配的设备号
        return ret;
    }

    // 成功时保存客户端信息
    dev_client = client;

    pr_info("dev device successfully probed\n");
    return 0;
}

定义ID 匹配表

static const struct i2c_device_id gtp_device_id[] = {
	{"myvendor,i2c_dev", 0},
	{}};

定义设备树匹配表

static const struct of_device_id dev_of_match_table[] = {
	{.compatible = "myvendor,i2c_dev"},
	{/* sentinel */}};

定义i2c总线设备结构体

struct i2c_driver dev_driver = {
	.probe = dev_probe,
	.remove = dev_remove,
	.id_table = gtp_device_id,
	.driver = {
		.name = "i2c_dev",
		.owner = THIS_MODULE,
		.of_match_table = dev_of_match_table,
	},
};
3、嵌入内核

./kernel/drivers/i2c/busses/Kconfig

# 新增以下内容
# def_bool y 为强制嵌入内核
config USER_dev
	tristate "User dev Driver"
	def_bool y
	depends on I2C
	help
		Enable support for the User dev Driver.

./kernel/drivers/i2c/busses/Makefile

# 新增以下内容
# obj-y 为强制嵌入内核
obj-y += user_dev.o
4、测试
  1. shell方式

    # 读取x00
    echo -n -e "\x00" > /dev/dev && dd if=/dev/dev bs=3 count=1 | xxd
    
    # 设置x01
    printf "\x31\x01" > /dev/dev
    
    # 查看节点状态权限
    ls -l /dev/dev
    chmod 666 /dev/dev
    # 手动设置节点
    mknod /dev/dev c 236 236
    # 查看节点日志
    dmesg | grep dev
    
    i2cdetect -y 2
    i2cget -y 2 0x18 0x30 b
    
  2. 代码方式

    编译以下代码 chmod添加权限后直接运行

    # Makefile
    # 设置交叉编译工具链路径
    CROSS_COMPILE =host/bin/aarch64-linux-
    
    # 设置编译器和链接器
    CC = $(CROSS_COMPILE)gcc
    CXX = $(CROSS_COMPILE)g++
    LD = $(CROSS_COMPILE)ld
    
    # 编译选项
    CFLAGS = -Wall -g
    
    # 源文件和目标文件
    SRC = read_test.c
    OBJ = $(SRC:.c=.o)
    EXE = read_test
    
    # 默认目标:编译程序
    all: $(EXE)
    
    # 编译可执行文件
    $(EXE): $(OBJ)
    	$(CC) $(OBJ) -o $(EXE)
    
    # 编译源文件为目标文件
    $(OBJ): $(SRC)
    	$(CC) $(CFLAGS) -c $(SRC)
    
    # 清理生成的文件
    clean:
    	rm -f $(OBJ) $(EXE)
    
    //read_test.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/ioctl.h>
    #include <errno.h>
    
    #define DEVICE_PATH "/dev/dev"  // 假设设备路径为/dev/dev
    
    // 函数:读取指定寄存器的数据
    int read_register(int fd, unsigned char reg_address, unsigned char *data, size_t length) {
        ssize_t ret;
    
        // 首先写寄存器地址
        ret = write(fd, &reg_address, 1);
        if (ret < 0) {
            perror("Failed to write register address");
            return -1;
        }
        // 读取指定长度的数据
        ret = read(fd, data, length);
        if (ret < 0) {
            perror("Failed to read data");
            return -1;
        }
        return 0;
    }
    
    int main() {
        int fd;
        unsigned char data[3] = { 0 };
    
        // 打开设备文件
        fd = open(DEVICE_PATH, O_RDWR);
        if (fd < 0) {
            perror("Failed to open device");
            return -1;
        }
    
        // 读取 0x00 寄存器的 3 字节数据
        data[0] = 0;
        if (read_register(fd, 0x00, data, 3) == 0) {
            printf("Read from register 0x00(Firmware version): ");
            for (int i = 0; i < 3; i++) {
                printf("0x%02x ", data[i]);
            }
            printf("\n");
        }
    
        // 读取 0x01 寄存器的 1 字节数据
        data[0] = 0x01;
        if (read_register(fd, 0x01, data, 1) == 0) {
            printf("Read from register 0x01(Hardware version): 0x%02x\n", data[0]);
        }
    
        // 关闭设备文件
        close(fd);
    
        return 0;
    }
    
posted @ 2025-05-22 14:28  暮云星影  阅读(31)  评论(0)    收藏  举报