当前位置: 首页 > news >正文

Day62 设备驱动程序开发基础与LED控制

day62 设备驱动程序开发基础与LED控制

本文档系统整理了设备驱动程序的核心概念、开发流程及实际应用,从理论到实践,涵盖字符设备驱动的注册、操作方法实现、硬件寄存器操作以及用户空间与内核空间的数据交互。


一、 驱动程序核心概念

1. 什么是驱动程序

驱动程序是操作和管理硬件的程序。在操作系统中,应用程序不能直接访问硬件,必须通过内核提供的驱动程序接口来间接操作硬件。

2. 系统架构分层

一个典型的计算机系统由硬件和软件构成,软件部分可进一步分为:

  • 操作系统 (OS):核心,负责管理系统资源。
  • 文件系统 (File System):建立在操作系统之上,提供文件管理功能。
  • 用户应用程序 (User App):运行在文件系统之上,通过系统调用与操作系统交互。
user app [open(led) fopen(led)]
---
rootfs [libc]
---
kernel [syscall_open(led)--led-->led 1\key 2\uart 3---1-->sys_open(1)--->1 2 3---open--->LED_DRV]
1 LED_DRV|2 KEY_DRV|3 UART_DRV
---
硬件
LED|KEY|UART
3. 驱动程序分类

根据数据访问方式和设备特性,驱动程序主要分为三类:

  • 字符设备驱动 (Character Device Driver):

    • 特点: 数据按字节流 (byte stream) 形式访问,即数据访问有严格的顺序性。
    • 示例: LED灯、按键、串口(UART)等。例如,控制LED灯亮灭必须按顺序执行“开-关-开”,无法跳过中间步骤。
    • 管理方式: 通过设备号 (Device Number) 管理。
  • 块设备驱动 (Block Device Driver):

    • 特点: 数据以固定大小的块 (block) 为单位进行访问,通常用于存储设备。
    • 示例: 硬盘、SSD、U盘等。访问时可以随机读取任意位置的数据,不受顺序限制。
    • 管理方式: 通过设备号 (Device Number) 管理。
  • 网络设备驱动 (Network Device Driver):

    • 特点: 集成复杂的协议栈 (Protocol Stack),如TCP/IP、UDP等。
    • 示例: 网卡。
    • 管理方式: 按照名字 (Name) 管理,如 ens33
4. 设备号 (Device Number)

设备号是内核用于管理驱动设备的一个32位无符号整数 (u32)。它被划分为两部分:

  • 主设备号 (Major Number): 高12位,用于区分设备类型。例如,所有LED灯使用同一个主设备号,所有按键使用另一个主设备号。
  • 次设备号 (Minor Number): 低20位,用于区分同一类型设备中的不同个体。例如,4个LED灯分别使用不同的次设备号(0, 1, 2, 3)。

注意: 网络设备没有设备号,而是通过名称管理。


二、 字符设备驱动开发流程

开发一个字符设备驱动程序需要完成以下四个核心步骤:

步骤1: 定义设备号

为驱动程序分配一个唯一的设备号。通常,主设备号从较大的数字开始分配(如255),因为较小的号码已被系统占用。

// 定义主设备号、次设备号和设备名称
#define MAJOR_NUM 248 // 主设备号
#define MINOR_NUM 0   // 次设备号
#define DEV_NAME "led1" // 设备名称
步骤2: 实现操作方法 (file_operations)

定义一个 struct file_operations 结构体,其中包含指向驱动程序具体操作函数的指针,如 open, read, write, release (close)。

// 声明操作函数原型
static int open(struct inode *node, struct file *file);
static ssize_t read(struct file *file, char __user *buf, size_t len, loff_t *loff);
static ssize_t write(struct file *file, const char __user *buf, size_t len, loff_t *loff);
static int close(struct inode *node, struct file *file);// 定义并初始化 file_operations 结构体
static struct file_operations fops = {.owner = THIS_MODULE, // 指向当前模块,用于防止模块在被引用时被卸载.open = open,         // 绑定 open 函数.read = read,         // 绑定 read 函数.write = write,       // 绑定 write 函数.release = close      // 绑定 release (close) 函数
};
步骤3: 绑定设备号与操作方法

创建一个 struct cdev 结构体,并将其与设备号和操作方法绑定。

static dev_t devno;          // 设备号变量
static struct cdev cdev;     // cdev 结构体实例// 在模块初始化函数中执行绑定
static int __init led1_init(void)
{int ret = 0;// 1. 使用 MKDEV 宏组合主、次设备号devno = MKDEV(MAJOR_NUM, MINOR_NUM);// 2. 初始化 cdev 结构体,关联操作方法cdev_init(&cdev, &fops);// 3. 将 cdev 添加到内核中,指定设备号和数量ret = cdev_add(&cdev, devno, 1);if(ret < 0)goto err_cdev_add; // 错误处理...
}
步骤4: 向内核注册驱动

调用 register_chrdev_region 函数,向内核申请并注册设备号范围。

// 在模块初始化函数中注册设备号
ret = register_chrdev_region(devno, 1, DEV_NAME); // 注册一个设备
if(ret < 0)goto err_register_chrdev; // 错误处理
步骤5: 清理与注销 (模块退出)

当模块被卸载时,必须反向执行上述步骤,释放资源。

static void __exit led1_exit(void)
{// 1. 取消映射的物理地址iounmap(gpio1_data);iounmap(gpio1_dir);iounmap(sw_pad);iounmap(sw_mux);// 2. 注销设备号unregister_chrdev_region(devno, 1);// 3. 删除 cdevcdev_del(&cdev);printk("led1_exit  ############################\n");
}

三、 用户空间与内核空间交互

1. 应用程序调用流程

用户程序通过标准库函数(如 open, read, write)发起系统调用,最终由内核调用驱动程序中对应的函数。

// 用户空间应用程序 (led1_app.c)
int main(int argc, const char *argv[])
{int fd = open("/dev/led1", O_RDWR); // 调用 open 系统调用if(fd < 0) { /* 错误处理 */ }write(fd, "ledon", 5);  // 调用 write 系统调用write(fd, "ledoff", 6); // 调用 write 系统调用close(fd); // 调用 close 系统调用return 0;
}
2. 驱动程序函数详解
  • open() 函数:
    • 作用: 初始化设备(如配置GPIO引脚),并建立设备与文件描述符的关联。
    • 返回值: 返回0表示成功,非0表示失败。注意: open 的返回值不是文件描述符,而是给内核判断是否成功的状态码。成功后,内核会分配一个文件描述符(通常是3, 4, 5…)返回给用户程序。
static int open(struct inode *node, struct file *file)
{led_init(); // 初始化LED硬件printk("kernel led open  ...\n");return 0; // 成功
}
  • read() 函数:
    • 作用: 从设备读取数据。对于LED驱动,此功能通常不需要实现。
    • 参数: file 文件结构体,buf 用户空间缓冲区指针,len 请求读取长度,loff 文件偏移量。
    • 返回值: 返回实际读取的字节数或错误码。
static ssize_t read(struct file *file, char __user *buf, size_t len, loff_t *loff)
{printk("kernel led read  ...\n");return 0; // 未实现读取功能
}
  • write() 函数:
    • 作用: 向设备写入数据,用于控制硬件行为(如开关LED)。
    • 关键点: 用户空间传入的指针 (buf) 是虚拟地址,不能直接在内核中使用。必须使用 copy_from_user 函数将数据从用户空间安全地拷贝到内核空间。
    • 返回值: 返回成功写入的字节数,或错误码(如 -EINVAL 表示无效参数)。
static ssize_t write(struct file *file, const char __user *buf, size_t len, loff_t *loff)
{int ret = 0;unsigned char data[10] = {0}; // 内核空间缓冲区// 计算要拷贝的实际长度,防止越界unsigned int len_cp = len < sizeof(data) ? len : sizeof(data);// 从用户空间拷贝数据到内核空间ret = copy_from_user(data, buf, len_cp);if(ret != 0) {printk("copy_from_user failed\n");return -EFAULT; // 拷贝失败}// 根据接收到的字符串控制LEDif(!strcmp(buf, "ledon"))led_on(); // 开灯else if(!strcmp(buf, "ledoff"))led_off(); // 关灯elseret = -EINVAL; // 无效参数printk("kernel led write  ...\n");return ret; // 返回写入的字节数或错误码
}
  • close() 函数:
    • 作用: 清理设备状态(如关闭LED),释放资源。
    • 返回值: 返回0表示成功,非0表示失败。
static int close(struct inode *node, struct file *file)
{led_off(); // 关闭LEDprintk("kernel led close  ...\n");return 0; // 成功
}

四、 硬件操作与寄存器访问

为了控制LED,驱动程序需要直接操作CPU的GPIO寄存器。由于这些寄存器位于物理地址空间,而内核代码运行在虚拟地址空间,因此需要使用 ioremap 函数进行地址映射。

1. 寄存器地址定义

根据芯片手册,定义相关寄存器的物理地址。

#define GPIO1_SW_MUX_CTL 0x20e0068    // IOMUXC_GPR_GPR1寄存器地址,用于设置引脚复用功能
#define GPIO1_SW_PAD_CTL 0x20e02f4    // IOMUXC_GPR_GPR1寄存器地址,用于设置引脚电气属性
#define GPIO1_DIR 0x209C004           // GPIO1_DR寄存器地址,用于设置方向(输入/输出)
#define GPIO1_DATA 0x209C000          // GPIO1_GDIR寄存器地址,用于设置数据(高/低电平)
2. 地址映射与硬件初始化

在驱动初始化时,使用 ioremap 将物理地址映射为内核可访问的虚拟地址。

static volatile unsigned long * sw_mux;
static volatile unsigned long * sw_pad;
static volatile unsigned long * gpio1_dir;
static volatile unsigned long * gpio1_data;static void led_init(void)
{// 配置引脚复用功能为GPIO模式*sw_mux &= ~(0xf << 0); // 清除低4位*sw_mux |= (0x5 << 0);  // 设置为GPIO模式 (0b0101)// 配置引脚电气属性*sw_pad = 0x10b0; // 设置为默认值// 设置GPIO方向为输出*gpio1_dir |= (1 << 3); // 第3位设为1,表示输出// 初始化LED为关闭状态 (高电平)*gpio1_data |= (1 << 3);
}static int __init led1_init(void)
{int ret = 0;...// 映射物理地址到虚拟地址sw_mux = ioremap(GPIO1_SW_MUX_CTL, 4);sw_pad = ioremap(GPIO1_SW_PAD_CTL, 4);gpio1_dir = ioremap(GPIO1_DIR, 4);gpio1_data = ioremap(GPIO1_DATA, 4);...
}
3. 控制LED亮灭

通过向数据寄存器写入特定值来控制LED。

static void led_on(void)
{*gpio1_data &= ~(1 << 3); // 清除第3位,拉低电平,点亮LED
}static void led_off(void)
{*gpio1_data |= (1 << 3); // 设置第3位,拉高电平,熄灭LED
}

volatile关键字: 用于修饰指针,告诉编译器该变量的值可能会被外部(如硬件)意外修改,禁止编译器对其进行优化,确保每次访问都从内存中读取真实值。


五、 编译、安装与测试

1. 修改内核配置

将驱动程序添加到内核构建系统中。

  • 修改 Makefile:

    # drivers/char/Makefile
    obj-$(CONFIG_LED1) += led1.o
    
  • 修改 Kconfig:

    # drivers/char/Kconfig
    config LED1bool "this is my led1"default yhelpthis is my first drivers, no use!
    
  • 配置内核:

    make menuconfig
    # 在菜单中选择 "Device Drivers" -> "Character devices" -> "this is my led1"
    # 保存并退出
    
2. 编译与部署
make zImage # 编译内核镜像
cp arch/arm/boot/zImage ~/tftpboot/ # 复制到TFTP服务器
3. 手动创建设备节点

在目标板上手动创建设备文件节点。

# 在目标板上执行
mknod /dev/led1 c 248 0 # 创建字符设备节点,主设备号248,次设备号0
4. 编译并运行测试程序
# 在主机上交叉编译应用程序
arm-linux-gnueabihf-gcc led1_app.c -o led1_app# 将应用程序复制到目标板的NFS根文件系统
scp led1_app root@<target_ip>:/nfs/imx6/rootfs/# 在目标板上运行程序
./led1_app
5. 测试结果

程序运行后,LED灯应按照程序逻辑闪烁(亮1秒,灭1秒)。同时,内核日志会输出相应的调试信息。

kernel led open ...
kernel led write ...
kernel led write ...
kernel led write ...
write to led1: Invalid argument
...

六、 错误处理与最佳实践

1. 错误处理

在驱动开发中,任何可能失败的操作(如 cdev_add, register_chrdev_region, ioremap)都必须进行错误检查,并在失败时清理已申请的资源。

static int __init led1_init(void)
{int ret = 0;...ret = cdev_add(&cdev, devno, 1);if(ret < 0)goto err_cdev_add;ret = register_chrdev_region(devno, 1, DEV_NAME);if(ret < 0)goto err_register_chrdev;sw_mux = ioremap(GPIO1_SW_MUX_CTL, 4);if(!sw_mux) {ret = -ENOMEM;goto err_ioremap;}... // 其他映射return 0;err_ioremap:iounmap(sw_mux);// ... 清理其他映射
err_register_chrdev:unregister_chrdev_region(devno, 1);
err_cdev_add:cdev_del(&cdev);printk("led1_init failed   ret = %d\n", ret);return ret;
}
2. 自动创建设备节点 (关键补充)

手动创建设备节点繁琐且易错。推荐使用 class_createdevice_create 函数让内核自动创建设备节点。

#include <linux/device.h> // 包含 class_create, device_create 等函数声明
// ... 其他必要的头文件// ... 其他定义
#define DEV_NAME "led1" // 设备节点名称 (这个名称将决定 /dev/ 下的文件名)
#define CLASS_NAME "led_class" // 设备类名称 (用于在 /sys/class/ 下创建类目录)static struct class *led_class;  // 用于指向设备类
static struct device *led_device; // 用于指向设备节点static int __init led1_init(void)
{...// 创建设备类led_class = class_create(THIS_MODULE, "led_class");if(IS_ERR(led_class)) {ret = PTR_ERR(led_class);goto err_class_create;}// 创建设备节点led_device = device_create(led_class, NULL, devno, NULL, DEV_NAME);if(IS_ERR(led_device)) {ret = PTR_ERR(led_device);goto err_device_create;}...
// 错误处理路径 (按相反顺序清理资源)
err_device_create:class_destroy(led_class); // 销毁类
err_class_create:// iounmap(gpio1_data); // 假设这里有 iounmap 调用// iounmap(gpio1_dir);// iounmap(sw_pad);// iounmap(sw_mux);// (注意: 如果 ioremap 在 device_create 之前,需要在这里添加 iounmap)
err_ioremap_sw_mux: // (假设 ioremap 在 device_create 之前)cdev_del(&cdev);
err_cdev_add:unregister_chrdev_region(devno, 1);
err_register_chrdev:printk("led1_init failed, ret = %d\n", ret);return ret; // 返回错误码   
}static void __exit led1_exit(void)
{// 销毁设备和类device_destroy(led_class, devno);class_destroy(led_class);...
}

关键问题解答: 为什么需要手动创建设备节点?
系统自带设备(如 /dev/ttyUSB0)无需手动创建,是因为它们的驱动程序内部已经实现了 class_createdevice_create 机制。我们的驱动程序缺少这个机制,所以需要手动执行 mknod 来创建节点。自动创建设备节点的关键在于 class_createdevice_create 这两个函数。设备节点的名称(如 led1)是由 device_create 函数的最后一个参数 DEV_NAME 决定的。你可以尝试修改 #define DEV_NAME "auto_led",然后重新编译加载驱动,观察 /dev 目录下生成的节点名称是否变为 auto_led


七、 总结与延伸

通过本日学习,我们掌握了字符设备驱动程序的基本框架和开发流程,包括设备号管理、操作方法实现、硬件寄存器操作以及用户空间与内核空间的数据交互。最终,我们成功实现了对LED灯的控制,验证了驱动程序的有效性。

后续学习建议:

  • 深入研究 ioctl 系统调用,实现更复杂的设备控制。
  • 学习中断处理机制,实现按键等外设的异步响应。
  • 探索 platform_driver 模型,使驱动程序与设备树(Device Tree)更好地集成。
  • 学习如何使用 udev 规则自动创建和管理设备节点。

重要提醒:

  • 拒绝图形化界面: 强烈建议使用命令行进行所有操作,这有助于深入理解底层原理,是嵌入式开发人员必备技能。
  • 别怕麻烦,多动手调试: 驱动开发过程复杂,遇到问题不要气馁,通过反复练习和调试,才能真正掌握。
  • 搜索关键词: “Linux 字符设备驱动自动创建设备节点”、“class_create”、“device_create”。
http://www.dtcms.com/a/482206.html

相关文章:

  • 支持Word (doc/docx) 和 PDF 转成一张垂直拼接的长PNG图片工具类
  • JAVA同城预约服务家政服务美容美发洗车保洁搬家维修家装系统源码小程序+公众号+h5
  • 正规拼多多代运营公司如何优化网站结构
  • 三层前馈神经网络实战:MNIST手写数字识别
  • 深度学习(四)
  • 学习HAL库STM32F103C8T6(MQTT报文)
  • 【C++】C++11特性学习(1)——列表初始化 | 右值引用与移动语义
  • 网站布局 种类手机商城页面设计
  • 如何建设手机端网站电力公司建设安全文化
  • 红色 VR 大空间:技术赋能红色文化传承的运营价值与实践路径
  • 网络协议工程 - eNSP及相关软件安装 - [eNSP, VirtualBox, WinPcap, Wireshark, Win7]
  • WHAT - 前端性能指标(交互和响应性能指标)
  • 专业的媒体发稿网
  • dede旅游网站模板wordpress教学主题
  • 做网站的技术性说明怎么自己做微网站吗
  • VScode安装以及C/C++环境配置20251014
  • 黄页网站大全通俗易懂wordpress 数据库配置错误
  • 常规的红外工业镜头有哪些?能做什么?
  • 一文读懂分子结合位点的预测:为双荧光素酶实验铺路
  • SM4密码核心知识点
  • 当代社会情绪分类及其改善方向深度解析
  • Python 求圆柱体的周长(Find the perimeter of a cylinder)
  • 攻防世界-Web-unseping
  • Python 第十三节 Python中各种输入输出方案详解及注意事项
  • 优秀的网站设计分析西电信息化建设处网站
  • 网页设计第6次课后作业
  • 算法---双指针一
  • ubuntu2404系统安装nocobase的方法
  • FFmpeg 播放播放 HTTP网络流读取数据过程分析
  • 使用Spring Boot构建系统安全层