学习笔记——Linux字符设备驱动开发

Linux字符设备驱动开发

一、设备驱动基本概念

1.1 什么是设备驱动?

设备驱动程序是操作系统内核中用于控制硬件设备的软件模块,它提供了应用程序访问硬件设备的接口。

类比理解

  • 硬件设备:打印机、U盘、鼠标等

  • 驱动程序:设备的"翻译官",让操作系统知道如何与设备对话

  • 应用程序:用户使用的软件(如Word文档打印)

1.2 软件层次结构

复制代码
应用程序层 (APP/Shell)
        ↓
    系统调用接口
        ↓
    操作系统内核 (Kernel)
        ↓
    设备驱动层 (Device Driver)
        ↓
    硬件设备层 (LED/KEY/UART等)

1.3 驱动程序的三大任务

  1. 实现硬件操作方法:open、read、write、close等

  2. 申请设备号:确保设备在系统中的唯一标识

  3. 向系统注册驱动模块:让内核知道这个驱动的存在

二、应用程序与驱动交互流程

2.1 打开设备流程

复制代码
应用程序:fd = open("/dev/led", O_RDWR)
         ↓
    内核层:sys_open()
         ↓
    查找设备:led→01, key→02, uart→03
         ↓
    找到LED驱动:调用01号设备的open()
         ↓
    驱动层:执行LED的open函数
         ↓ 返回
    内核层:创建PCB记录fd=3
         ↓ 返回
应用程序:获得文件描述符fd=3

2.2 读写设备流程

复制代码
应用程序:write(fd, data, size)
         ↓
    内核层:根据fd=3找到对应驱动
         ↓
    驱动层:执行LED驱动的write函数
         ↓
    硬件层:控制LED亮灭

三、设备驱动分类

3.1 字符设备驱动(90%的设备)

特点 :数据按字节流顺序访问
例子:键盘、鼠标、串口、LED

3.2 块设备驱动

特点 :数据可以随机访问
例子:硬盘、U盘、SD卡

3.3 网络设备驱动

特点 :集成复杂的网络协议栈,没有设备号
例子:网卡、WiFi模块

四、设备号详解

4.1 设备号结构

设备号是一个32位无符号整数:

  • 高12位:主设备号(代表设备类型/功能)

  • 低20位:次设备号(同类设备中的不同个体)

例子

复制代码
主设备号255 + 次设备号0 → 设备号 = (255 << 20) | 0

4.2 地址空间

  • 应用程序:操作虚拟地址(保护模式)

  • 驱动程序:操作物理地址(直接控制硬件)

  • 操作系统作用:隔离硬件,提供安全访问

4.3 重要规范

内核编程注意:尽量避免使用浮点数打印,以提高效率

五、第一个字符设备驱动实战

5.1 创建驱动源文件

复制代码
/* drivers/char/demo_driver.c */
#include <linux/init.h>     // 模块初始化相关
#include <linux/module.h>   // 模块相关
#include <linux/printk.h>   // 内核打印函数
#include <linux/fs.h>       // 文件系统相关
#include <linux/cdev.h>     // 字符设备结构体
#include <linux/kdev_t.h>   // 设备号相关

// 定义设备信息
#define DEV_MAJOR 255      // 主设备号
#define DEV_MINOR 0        // 次设备号  
#define DEV_NAME "demo1"   // 设备名称

// 设备操作函数
static int demo_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "demo1: open called\n");
    return 0;
}

static ssize_t demo_read(struct file *file, char __user *buf, 
                         size_t size, loff_t *offset)
{
    printk(KERN_INFO "demo1: read called, size=%zu\n", size);
    return 0;
}

static ssize_t demo_write(struct file *file, const char __user *buf,
                          size_t size, loff_t *offset)
{
    printk(KERN_INFO "demo1: write called, size=%zu\n", size);
    return size;  // 返回实际写入的字节数
}

static int demo_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "demo1: release called\n");
    return 0;
}

// 定义文件操作结构体
static struct file_operations demo_fops = {
    .owner = THIS_MODULE,    // 防止模块在使用时被卸载
    .open = demo_open,       // 打开设备
    .read = demo_read,       // 读设备
    .write = demo_write,     // 写设备
    .release = demo_release, // 关闭设备
};

// 全局变量
static dev_t dev_num;          // 设备号
static struct cdev demo_cdev;  // 字符设备结构体

// 模块初始化函数
static int __init demo_init(void)
{
    int ret = 0;
    
    // 1. 生成设备号
    dev_num = MKDEV(DEV_MAJOR, DEV_MINOR);
    
    // 2. 注册设备号区域
    ret = register_chrdev_region(dev_num, 1, DEV_NAME);
    if (ret < 0) {
        printk(KERN_ERR "demo1: register chrdev region failed\n");
        goto err_register;
    }
    
    // 3. 初始化字符设备
    cdev_init(&demo_cdev, &demo_fops);
    demo_cdev.owner = THIS_MODULE;
    
    // 4. 添加字符设备到系统
    ret = cdev_add(&demo_cdev, dev_num, 1);
    if (ret < 0) {
        printk(KERN_ERR "demo1: cdev add failed\n");
        goto err_cdev_add;
    }
    
    printk(KERN_INFO "demo1: driver initialized successfully\n");
    return 0;

// 错误处理
err_cdev_add:
    cdev_del(&demo_cdev);
err_register:
    unregister_chrdev_region(dev_num, 1);
    return ret;
}

// 模块退出函数
static void __exit demo_exit(void)
{
    // 1. 从系统删除字符设备
    cdev_del(&demo_cdev);
    
    // 2. 注销设备号
    unregister_chrdev_region(dev_num, 1);
    
    printk(KERN_INFO "demo1: driver removed\n");
}

// 模块声明
module_init(demo_init);  // 指定初始化函数
module_exit(demo_exit);  // 指定退出函数

MODULE_LICENSE("GPL");           // 许可证
MODULE_AUTHOR("Your Name");      // 作者
MODULE_DESCRIPTION("Demo Driver"); // 描述

5.2 配置编译系统

修改Kconfig文件
复制代码
# drivers/char/Kconfig 中添加:
config DEMO_DRIVER
    tristate "This is a demo_driver"
    default y
    help
      This is a demo_driver
修改Makefile文件
复制代码
# drivers/char/Makefile 中添加:
obj-$(CONFIG_DEMO_DRIVER) += demo_driver.o

5.3 编译和测试

编译步骤:

bash

复制代码
# 1. 进入内核源码目录
cd ~/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek

# 2. 配置内核(选择Demo driver)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 进入:Device Drivers → Character devices → Demo test driver
# 选择:<M> 编译为模块 或 <*> 编译进内核

# 3. 编译模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
# 或编译特定模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- M=drivers/char modules
测试应用程序:
复制代码
/* demo_app.c - 测试程序 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>

int main(void)
{
    int fd;
    char buf[32] = "Hello Driver";
    char read_buf[32] = {0};
    
    // 1. 打开设备
    fd = open("/dev/demo1", O_RDWR);
    if (fd < 0) {
        perror("Open device failed");
        return -1;
    }
    printf("Device opened successfully, fd=%d\n", fd);
    
    // 2. 写入数据
    if (write(fd, buf, strlen(buf)) < 0) {
        perror("Write failed");
        close(fd);
        return -1;
    }
    printf("Write: %s\n", buf);
    
    // 3. 读取数据
    if (read(fd, read_buf, sizeof(read_buf)) < 0) {
        perror("Read failed");
        close(fd);
        return -1;
    }
    printf("Read: %s\n", read_buf);
    
    // 4. 关闭设备
    close(fd);
    printf("Device closed\n");
    
    return 0;
}
手动创建设备节点:
复制代码
# 在开发板上执行:
# 1. 加载驱动模块
insmod demo_driver.ko

# 2. 查看分配的设备号
cat /proc/devices | grep demo1

# 3. 创建设备节点(假设主设备号是255)
mknod /dev/demo1 c 255 0

# 参数解释:
# /dev/demo1 - 设备节点路径(应用程序open的参数)
# c          - 字符设备类型
# 255        - 主设备号(与驱动中DEV_MAJOR一致)
# 0          - 次设备号(与驱动中DEV_MINOR一致)

# 4. 修改权限
chmod 666 /dev/demo1  # 让所有用户可读写

# 5. 编译运行测试程序
arm-linux-gnueabihf-gcc demo_app.c -o demo_app
./demo_app

六、开发工具使用

6.1 ctags代码跳转工具

复制代码
# 在内核源码目录生成索引
ctags -R

# Vim中使用:
# ctrl + ]  - 跳转到定义
# ctrl + o  - 跳转回之前位置
# :ts        - 显示所有匹配的标签
# :tn/:tp   - 下一个/上一个匹配

6.2 调试技巧

  1. 查看内核打印信息
复制代码
dmesg | tail -20          # 查看最后20条内核日志
dmesg -c                  # 查看并清空日志
cat /proc/kmsg &          # 实时查看内核消息
  1. 查看已注册设备
复制代码
cat /proc/devices         # 查看所有注册的设备
ls -l /dev/               # 查看设备节点
lsmod                    # 查看已加载模块

七、驱动框架总结

7.1 标准字符设备驱动流程

复制代码
初始化阶段:
1. 定义file_operations结构体
2. 分配设备号(静态或动态)
3. 初始化cdev结构体
4. 添加cdev到系统
5. 创建设备节点(手动或自动)

运行阶段:
1. 应用程序调用open()
2. 内核查找对应驱动
3. 调用驱动的open函数
4. 后续read/write/ioctl操作
5. 调用close释放资源

退出阶段:
1. 删除cdev
2. 注销设备号
3. 删除设备节点

7.2 关键数据结构

  • file_operations:定义设备操作函数

  • cdev:字符设备内核结构

  • inode:文件系统索引节点

  • file:打开文件的结构体

7.3 常用函数

复制代码
// 设备号操作
MKDEV(major, minor)           // 生成设备号
MAJOR(dev_t dev)              // 提取主设备号
MINOR(dev_t dev)              // 提取次设备号

// 设备注册
register_chrdev_region()      // 静态注册设备号
alloc_chrdev_region()         // 动态分配设备号
unregister_chrdev_region()    // 注销设备号

// 字符设备操作
cdev_init()                   // 初始化cdev
cdev_add()                    // 添加cdev到系统
cdev_del()                    // 删除cdev

// 内核打印
printk(KERN_LEVEL "message")  // 内核打印函数

八、常见问题解决

8.1 设备号冲突

复制代码
# 查看已使用的设备号
cat /proc/devices

# 解决方法:
# 1. 选择未使用的设备号
# 2. 使用动态分配:alloc_chrdev_region()

8.2 权限问题

复制代码
# 创建设备节点后设置权限
sudo chmod 666 /dev/demo1

# 或者在驱动中设置默认权限
# 使用class_create()和device_create()自动创建设备节点

8.3 模块加载失败

复制代码
# 查看详细错误
dmesg | tail -20

# 常见错误:
# 1. 设备号被占用
# 2. 符号未导出
# 3. 内存分配失败
相关推荐
charlie1145141912 小时前
嵌入式C++教程——ETL(Embedded Template Library)
开发语言·c++·笔记·学习·嵌入式·etl
czhaii2 小时前
STC32G.H中文注释各寄存器特殊功能寄存器作用
单片机·嵌入式硬件
码农三叔2 小时前
(9-3)电源管理与能源系统:充电与扩展能源方案
人工智能·嵌入式硬件·机器人·能源·人形机器人
Lw老王要学习2 小时前
CentOS 7.9达梦数据库安装全流程解析
linux·运维·数据库·centos·达梦
李小星同志2 小时前
VID2WORLD: CRAFTING VIDEO DIFFUSION MODELSTO INTERACTIVE WORLD MODELS论文学习
学习
m0_736919102 小时前
C++中的享元模式变体
开发语言·c++·算法
集芯微电科技有限公司2 小时前
15V/2A同步开关型降压单节/双节锂电池充电管理IC支持输入适配器 DPM 功能
c语言·开发语言·stm32·单片机·嵌入式硬件·电脑
罗湖老棍子2 小时前
【 例 1】石子合并(信息学奥赛一本通- P1569)
数据结构·算法·区间dp·区间动态规划·分割合并
CRUD酱2 小时前
CentOS的yum仓库失效问题解决(换镜像源)
linux·运维·服务器·centos