Linux驱动开发笔记(零)驱动基础知识及准备

本文是Linux驱动开发系列第一期,归纳前期需准备的知识。介绍了Linux、MCU和FPGA编程的区别,阐述Linux内核模块,包括其定义、代码架构、头文件、模块参数和makefile说明。还讲解了驱动程序设计思路,如基本步骤、设备号、重要数据结构及数据访问函数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

  在简单结束应用层的开发学习后,本系列将开启驱动层的学习,本文作为该系列第一期旨在归纳前期需要准备的知识。

一、Liunx、MCU和FPGA编程的区别

  FPGA的编程更多地依赖于硬件描述语言(HDL),如VHDL或Verilog,用于描述电路的结构和行为。这种编程方法将硬件电路抽象为逻辑门、寄存器等基本元素,并通过编写代码来描述它们之间的连接关系和功能。相较于其他两种编程方式,FPGA编程需要频繁抓取时钟,这是因为FPGA的并行性、硬件描述方式、精确的时序控制需求以及性能优化等方面的要求。这种时钟抓取机制使得FPGA能够高效地处理复杂的数字逻辑任务,并在各种应用中实现高性能和灵活性。
  相比之下,MCU的编程通常更侧重于顺序执行和寄存器的直接操作。因为MCU的操作是顺序执行的,开发者可以通过直接读写寄存器来管理内部状态和操作。这种编程方式相对简单直观,但可能不如FPGA在并行处理和复杂逻辑实现上灵活。
  至于Linux框架下的驱动编程,它与FPGA和单片机的编程方式有显著的不同。Linux驱动编程主要关注于设备与操作系统之间的交互,包括设备节点的创建、设备驱动程序的编写和与操作系统的接口实现等。驱动程序需要处理设备的硬件特性,并将其抽象为操作系统可以理解和操作的接口。这种编程方式更注重于软件的架构和接口设计,与硬件的交互通常是通过特定的接口和协议来实现的。

二、Linux内核模块

  在Linux系统中,设备驱动会以内核模块的形式出现,学习Linux内核模块编程是驱动开发的先决条件。 第一次接触Linux内核模块,我们将围绕着“Linux内核模块是什么”,“Linux内核模块的工作原理”以及 “我们该怎么使用Linux内核模块”这样的思路一起走进Linux内核世界。

1. 什么是内核模块

  Linux内核模块是Linux内核向外部提供的一个插口,也被称为动态可加载内核模块(Loadable Kernel Module,LKM)。它是一个具有独立功能的程序,可以被单独编译,但不能独立运行。在运行时,内核模块被链接到内核作为内核的一部分在内核空间运行。
  内核模块的主要作用是扩展内核的功能,而无需重新编译整个内核。例如,内核模块通常用于添加新的设备驱动程序、文件系统或其他功能到内核中。通过内核模块,Linux内核能够实现内核功能扩展,提供新的系统调用或特性,甚至实现内核的安全增强以增加系统的安全性。
  这里展示一张图片可以让大家直观地感受一下Linux的内核体系(Monolithic Kernel)。
在这里插入图片描述
  可以看到Linux所使用的宏内核架构是将包括微内核(Microkernel)以及微内核之外的应用层IPC、文件系统功能、设备驱动模块都编译成一个整体。 其优点是执行效率非常高,但缺点也是十分明显的,一旦我们想要修改、增加内核某个功能时(如增加设备驱动程序)都需要重新编译一遍内核。 Linux操作系统正是采用了宏内核结构。为了解决这一缺点,linux中引入了内核模块这一机制。

2. 内核模块的代码架构

  Linux内核模块的代码框架通常由下面几个部分组成:

  • 模块加载函数(必须): 当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
  • 模块卸载函数(必须): 当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。
  • 模块许可证声明(必须): 许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。
  • 模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。
  • 模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
  • 模块的其他相关信息: 可以声明模块作者等信息。
参数 功能
MODULE_LICENSE() 表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可
MODULE_AUTHOR() 描述模块的作者信息
MODULE_DESCRIPTION() 对模块的简单介绍
MODULE_ALIAS() 给模块设置一个别名

  这里给出一个简单的示例(注:仅作介绍使用):

#include <linux/module.h>   // 包含内核模块所需的头文件  
#include <linux/kernel.h>  
 
// 模块许可证声明,这是必须的,且必须是这种形式的宏定义  
MODULE_LICENSE("Dual BSD/GPL");  
 
// 模块初始化函数,当模块被加载时调用  
static int __init my_module_init(void)  
{
   
     
   // 初始化代码,比如申请资源、注册设备驱动等  
   printk(KERN_INFO "My module has been loaded.\n");  
     
   // 返回0表示初始化成功,非0值表示失败  
   return 0;  
}  
 
// 模块清理函数,当模块被卸载时调用  
static void __exit my_module_exit(void)  
{
   
     
   // 清理代码,比如释放资源、注销设备驱动等  
   printk(KERN_INFO "My module has been unloaded.\n");  
}  
 
// 使用module_init和module_exit宏来指定初始化函数和清理函数  
module_init(my_module_init);  
module_exit(my_module_exit);

3. 头文件

  前面我们已经接触过了Linux的应用编程,了解到Linux的头文件都存放在/usr/include中。 编写内核模块所需要的头文件,并不在上述说到的目录,而是在Linux内核源码中的include文件夹。编写内核模块中经常要使用到的头文件有以下两个:<linux/init.h>和<linux/module.h>。

#include <linux/module.h>//包含内核模块信息声明的相关函数
#include <linux/init.h>//包含了 module_init()和 module_exit()函数的声明
#include <linux/kernel.h>//包含内核提供的各种函数,如printk

4. 模块参数

  模块参数允许用户在加载模块时通过命令行指定参数值。这些参数在模块的加载过程中被获取,并转换成相应类型的值,然后赋值给对应的变量,这个过程常常发生在函数调用之前。

module_param(name, type, perm)
/*
name: 我们定义的变量名
type: 参数的类型,目前内核支持的参数类型有byte,short,ushort,int,uint,long,ulong,charp,bool,
   invbool。其中charp表示的是字符指针,bool是布尔类型,其值只能为0或者是1;invbool是反布尔类型,其值
   也是只能取0或者是1,但是true值表示0,false表示1。变量是char类型时,传参只能是byte,char * 时只能是
   charp
perm: 表示的是该文件的权限,详细参考下表
*/
用户 参数 功能
当前用户 S_IRUSR 用户拥有读权限
S_IWUSR 用户拥有写权限
当前用户组 S_IRGRP 当前用户组的其他用户拥有读权限
S_IWUSR 当前用户组的其他用户拥有写权限
其他用户 S_IROTH 其他用户拥有读权限
S_IWOTH 其他用户拥有写权限

  模块参数的使用通常涉及以下步骤:

  • 在模块代码中声明变量,并使用module_param()宏或module_param_named()宏来定义模块参数。这些宏接受三个参数:变量名、变量类型以及访问权限。访问权限用于定义在sysfs中对应文件的访问权限,与Linux文件访问权限的管理方式相同。
  • 在加载模块时,用户可以通过命令行传递参数值给模块。这些值将被转换为相应的类型,并赋值给在模块代码中声明的变量。

5. makefile说明

  对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。 为此,我们在编译时需要到内核源码目录下进行编译。 编译内核模块使用的Makefile文件,和我们前面编译C代码使用的Makefile大致相同, 这得益于编译Linux内核所采用的Kbuild系统,因此在编译内核模块时,我们也需要指定环境变量ARCH和CROSS_COMPILE的值。

KERNEL_DIR=../../../kernel/

ARCH=arm64
CROSS_COMPILE=aarch64-linux-gnu-
export  ARCH  CROSS_COMPILE

obj-m := XXX.o
all:
   $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules

.PHONE:clean

clean:
   $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

  以上代码中提供了一个关于编译内核模块的Makefile。
  第1行:该Makefile定义了变量KE

<think>好的,我现在需要给用户介绍一下Linux驱动开发的入门知识。用户可能对这方面不太熟悉,所以得从基础开始讲起,同时要确保内容清晰易懂。首先,我需要回忆一下Linux驱动开发的基本概念,包括驱动的作用、分类,以及开发环境的搭建步骤。 接下来,得考虑用户的学习路径。通常,学习驱动开发需要有一定的C语言和操作系统基础,特别是对Linux内核有一定的了解。所以可能需要先提到这些前置知识,让用户评估自己是否准备好。 然后,应该分步骤介绍入门流程,比如环境准备驱动类型、代码结构、编译加载方法、调试技巧等。这部分需要结构化,用列表或者分点说明,方便用户一步步跟随。同时,要给出一个简单的示例代码,比如Hello World模块,并解释关键部分,比如模块的初始化和退出函数,还有许可证声明。 还要注意驱动开发中的常见问题,比如内核编程与用户空间的区别,比如不能使用标准库函数,需要注意并发和内存管理。这些是新手容易犯错的地方,需要特别提醒。 可能还需要推荐一些学习资源,比如书籍、在线文档和社区,这样用户可以有进一步学习的途径。另外,强调实践的重要性,建议从简单例子开始,逐步深入。 在组织内容时,要确保逻辑清晰,先讲概念,再讲准备,接着是开发步骤,示例,注意事项,最后是学习资源。避免使用过于专业的术语,或者在使用时给出简单解释。同时,检查是否有遗漏的重要点,比如内核版本差异,或者设备树(Device Tree)在较新内核中的使用,这些可能需要提及。 最后,保持语言简洁,用口语化的中文,避免冗长,让用户能轻松理解。需要确认所有步骤都是当前通用的方法,不会因为内核版本过旧而失效。如果有不确定的地方,比如具体的内核API函数,可能需要查阅最新文档确保准确性。</think>Linux 驱动开发是嵌入式系统和操作系统领域的重要技能,以下是入门指南,帮助你理解基本概念和上手实践: --- ### **1. 前置知识** - **C语言基础**:驱动开发主要使用C语言(尤其是内核风格的C)。 - **Linux操作系统基础**:熟悉Linux命令行、文件系统、权限管理等。 - **计算机体系结构**:了解CPU、内存、外设(如GPIO、I2C、USB)的工作原理。 - **内核基础**:理解进程管理、内存管理、中断、内核模块等概念。 --- ### **2. Linux驱动的作用** 驱动是内核与硬件设备的桥梁,负责: - 初始化硬件(如配置寄存器) - 实现设备的数据读写 - 处理中断和DMA操作 - 通过标准接口(如`/dev`、`sysfs`)向用户空间暴露功能 --- ### **3. 驱动分类** - **字符设备**(如键盘、传感器):按字节流访问,常用 `open()`, `read()`, `write()` 接口。 - **块设备**(如硬盘):按数据块访问,支持缓存。 - **网络设备**(如网卡):通过套接字(Socket)通信。 - **平台设备**(如SoC内置外设):与芯片紧密相关,常用设备树(Device Tree)描述。 --- ### **4. 开发环境搭建** - **Linux系统**:推荐Ubuntu或Fedora(安装内核头文件:`sudo apt install linux-headers-$(uname -r)`)。 - **工具链**:GCC、Make、GDB调试工具。 - **内核源码**:下载与当前内核版本匹配的源码([kernel.org](https://www.kernel.org/))。 --- ### **5. 第一个驱动:Hello World** ```c #include <linux/init.h> #include <linux/module.h> // 模块加载函数 static int __init hello_init(void) { printk(KERN_INFO "Hello, Linux Driver!\n"); return 0; } // 模块卸载函数 static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye!\n"); } // 注册模块入口和出口 module_init(hello_init); module_exit(hello_exit); // 模块信息 MODULE_LICENSE("GPL"); // 开源协议(必须) MODULE_AUTHOR("Your Name"); // 作者 MODULE_DESCRIPTION("A simple example"); // 描述 ``` **编译驱动**: 编写 `Makefile`(注意使用内核的编译系统): ```makefile obj-m += hello.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean ``` **加载/卸载驱动**: ```bash sudo insmod hello.ko # 加载驱动 dmesg | tail # 查看内核日志 sudo rmmod hello # 卸载驱动 ``` --- ### **6. 驱动开发核心步骤** 1. **定义设备操作接口**:实现 `file_operations` 结构体(如 `.open`, `.read`, `.ioctl`)。 2. **注册设备**:使用 `register_chrdev()` 注册字符设备。 3. **处理硬件交互**:通过I/O端口、内存映射或内核API(如 `ioremap()`)访问硬件。 4. **处理中断**:注册中断处理函数 `request_irq()`。 5. **用户空间交互**:通过 `/dev/` 设备文件或 `sysfs` 提供访问接口。 --- ### **7. 调试技巧** - **printk**:内核日志输出(查看:`dmesg`)。 - **GDB + KGDB**:内核调试(需要配置调试内核)。 - **仿真工具**:QEMU模拟硬件环境。 - **动态探测**:`strace`、`perf` 跟踪系统调用。 --- ### **8. 学习资源** - **书籍**: - 《Linux设备驱动程序》(Linux Device Drivers, 简称LDD) - 《Linux内核设计与实现》 - **文档**: - [内核官方文档](https://www.kernel.org/doc/html/latest/) - [设备树(Device Tree)使用手册](https://www.devicetree.org/) - **实践项目**:从LED驱动、按键中断等简单硬件开始,逐步深入复杂设备(如SPI、USB)。 --- ### **9. 注意事项** - **内核编程限制**:不能使用用户空间的库(如glibc),需用内核API。 - **并发与同步**:注意多线程、中断上下文,使用信号量、互斥锁(`mutex`)等。 - **内存管理**:使用 `kmalloc()`/`kfree()` 而非 `malloc()`/`free()`,注意内存泄漏。 --- 通过以上步骤,你可以逐步掌握驱动开发的核心逻辑。建议从简单的字符设备入手,结合硬件实践,同时多阅读内核源码(如 `drivers/char/` 目录下的示例)。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值