嵌入式Linux字符设备驱动开发
嵌入式Linux字符设备驱动开发
一、字符设备驱动基础概念
1. 字符设备定义
字符设备是Linux驱动开发中最基本的一类设备驱动,以字节流形式进行读写操作,读写数据是分先后顺序的。常见字符设备包括:
- 点灯
- 按键
- IIC
- SPI
- LCD等
2. 应用程序与驱动调用关系
Linux应用程序通过标准API调用驱动程序,流程如下:
open()、close()、read()、write()等API
↓
C库函数
↓
系统调用进入内核
↓
内核驱动程序中的open()、close()、read()、release()等函数
↓
具体硬件设备
3. file_operations结构体
这是Linux内核驱动操作函数集合,定义在include/linux/fs.h
中,包含以下重要函数:
函数 | 描述 |
---|---|
owner | 指向拥有该结构体的模块 |
llseek | 修改文件当前读写位置 |
read | 读取设备文件 |
write | 向设备文件写入数据 |
poll | 查询设备是否可进行非阻塞读写 |
unlocked_ioctl | 设备控制功能 |
mmap | 将设备内存映射到进程空间 |
open | 打开设备文件 |
release | 关闭设备文件 |
二、字符设备驱动开发步骤
1. 驱动模块加载与卸载
Linux驱动可编译进内核或编译为模块(.ko)。模块开发更便于调试,只需加载/卸载模块即可。
static int __init xxx_init(void) {// 初始化代码return 0;
}static void __exit xxx_exit(void) {// 清理代码
}module_init(xxx_init);
module_exit(xxx_exit);
2. 字符设备注册与注销
使用register_chrdev
和unregister_chrdev
函数注册和注销字符设备:
static int __init chrdevbase_init(void) {int retvalue = 0;retvalue = register_chrdev(CHARDEVBASE_MAJOR, CHARDEVBASE_NAME, &chrdevbase_fops);if(retvalue < 0) {// 错误处理}return 0;
}static void __exit chrdevbase_exit(void) {unregister_chrdev(CHARDEVBASE_MAJOR, CHARDEVBASE_NAME);
}
3. 设备操作函数实现
实现file_operations结构体中的具体操作函数:
static int chardevbase_open(struct inode *inode, struct file *file) {return 0;
}static int chardevbase_release(struct inode *inode, struct file *file) {return 0;
}static ssize_t chardevbase_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {// 实现读操作return 0;
}static ssize_t chardevbase_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {// 实现写操作return count;
}
4. LICENSE和作者信息添加
必须添加模块许可证信息,否则编译会报错:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
三、设备号管理
1. 设备号组成
设备号由主设备号和次设备号组成:
- 主设备号:表示具体驱动,高12位
- 次设备号:表示使用该驱动的各个设备,低20位
2. 设备号分配方法
- 静态分配:直接指定设备号
- 动态分配:使用
alloc_chrdev_region
自动分配
四、chrdevbase字符设备驱动开发实验
1. 驱动实现
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>#define CHARDEVBASE_MAJOR 200
#define CHARDEVBASE_NAME "chardevbase"char readbuf[100]; // 读缓冲区
char writebuf[100]; // 写缓冲区
char kerneldata[] = {"kernel data"}; // 内核数据// 打开设备
static int chardevbase_open(struct inode *inode, struct file *file) {return 0;
}// 关闭设备
static int chardevbase_release(struct inode *inode, struct file *file) {return 0;
}// 读取设备
static ssize_t chardevbase_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) {int ret = 0;memcpy(readbuf, kerneldata, sizeof(kerneldata));ret = copy_to_user(buf, readbuf, count);if (ret < 0) {printk(KERN_ALERT "Failed to copy data to user space\n");return -EFAULT;}return ret;
}// 写入设备
static ssize_t chardevbase_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) {int ret = 0;ret = copy_from_user(writebuf, buf, count);if (ret < 0) {printk(KERN_ALERT "Failed to copy data from user space\n");return -EFAULT;} else {writebuf[count] = '\0';printk(KERN_INFO "Kernel received from user: %s\n", writebuf);}return count;
}// 设备操作函数集合
static struct file_operations chardevbase_fops = {.owner = THIS_MODULE,.open = chardevbase_open,.release = chardevbase_release,.read = chardevbase_read,.write = chardevbase_write,
};// 驱动入口
static int __init chardevbase_init(void) {int ret = 0;printk(KERN_INFO "Character Device Base Module Initialized\n");ret = register_chrdev(CHARDEVBASE_MAJOR, CHARDEVBASE_NAME, &chardevbase_fops);if (ret < 0) {printk(KERN_ALERT "Failed to register character device\n");return ret;}return 0;
}// 驱动出口
static void __exit chardevbase_exit(void) {printk(KERN_INFO "Character Device Base Module Exited\n");unregister_chrdev(CHARDEVBASE_MAJOR, CHARDEVBASE_NAME);
}module_init(chardevbase_init);
module_exit(chardevbase_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
2. 测试应用程序
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>int main(int argc, char *argv[]) {if (argc < 3) {fprintf(stderr, "Usage: %s <device_file>\n", argv[0]);return -1;}int fd = 0;char *filename = argv[1];fd = open(filename, O_RDWR);if (fd < 0) {perror("Failed to open character device");return -1;}int ret = 0;if (atoi(argv[2]) == 1) { // 读操作char readbuf[100];ret = read(fd, readbuf, sizeof(readbuf));if (ret < 0) {perror("Failed to read from character device");close(fd);return -1;} else {printf("User read from character device: %s\r\n", readbuf);}} else if(atoi(argv[2]) == 2) { // 写操作char writebuf[100] = "Hello from user write buffer!";ret = write(fd, writebuf, strlen(writebuf));if (ret < 0) {perror("Failed to write to character device");close(fd);return -1;}}ret = close(fd); // 关闭设备if (ret < 0) {perror("Failed to close character device");return -1;}return 0;
}
3. Makefile
KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)obj-m := chardevbase.obuild : kernel_moduleskernel_modules:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modulesclean:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
五、驱动测试步骤
-
编译驱动模块
make
-
加载驱动模块
insmod chardevbase.ko # 或 modprobe chardevbase.ko
-
创建设备节点文件
mknod /dev/chardevbase c 200 0
-
测试读操作
./chardevbaseAPP /dev/chardevbase 1
-
测试写操作
./chardevbaseAPP /dev/chardevbase 2
-
卸载驱动模块
rmmod chardevbase.ko
六、关键知识点总结
-
内核空间与用户空间交互
- 使用
copy_to_user()
将数据从内核空间复制到用户空间 - 使用
copy_from_user()
将数据从用户空间复制到内核空间
- 使用
-
printk与printf区别
printk
用于内核空间输出printf
用于用户空间输出printk
支持消息级别分类
-
设备文件操作
- 应用程序通过
/dev
目录下的设备文件操作硬件 - 设备文件通过
mknod
命令创建
- 应用程序通过
-
模块调试技巧
- 使用
insmod
或modprobe
加载模块 - 使用
rmmod
卸载模块 - 使用
dmesg
查看内核日志
- 使用
-
设备号管理
- 使用
cat /proc/devices
查看已使用设备号 - 推荐使用动态分配设备号
- 使用
gitee源码仓库