七、新增设备驱动
新增设备驱动
2.7.1、编写规则
在 Linux 中,设备驱动程序是内核模块,用于实现内核与硬件设备之间的通信。设备驱动主要分为字符设备驱动、块设备驱动和网络设备驱动。
- 字符设备驱动:以字节流的形式处理数据,如串口、键盘等。
- 块设备驱动:以块为单位处理数据,如硬盘。
- 网络设备驱动:用于网络通信。
1、编写规范
-
头文件包含
内核驱动需要包含内核特定的头文件,这些头文件提供了内核数据结构、函数和宏的定义。常见的头文件有:
#include <linux/init.h> // 模块初始化和退出相关 #include <linux/module.h> // 模块编程基本定义 #include <linux/fs.h> // 文件系统相关定义 #include <linux/uaccess.h> // 用户空间和内核空间数据交互
-
模块初始化和退出函数
每个内核模块都需要定义初始化和退出函数。
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
标记的函数在模块卸载时执行。
-
模块许可证声明
必须声明模块的许可证,常见的是 GPL 许可证。
MODULE_LICENSE("GPL");
-
设备注册和注销
对于字符设备驱动,需要注册和注销字符设备。
static int major_number; major_number = register_chrdev(0, "my_device", &fops); // 注册字符设备 unregister_chrdev(major_number, "my_device"); // 注销字符设备
register_chrdev
第一个参数为 0 表示让内核自动分配主设备号。fops
是文件操作结构体。
-
文件操作结构体
定义设备的读写等操作函数。
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, };
-
用户空间和内核空间数据交互
使用
copy_to_user
和copy_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、加载和卸载驱动模块
-
加载模块
使用
insmod
命令加载驱动模块:sudo insmod mychardevice.ko
-
查询模块
使用
lsmod
命令查看已加载的模块:lsmod | grep mychardevice
-
卸载模块
使用
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(®_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、测试
-
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
-
代码方式
编译以下代码 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, ®_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; }