Linux驱动开发实战(一):LED控制驱动详解
Linux驱动开发野火实战(一):LED控制驱动详解
文章目录
- Linux驱动开发野火实战(一):LED控制驱动详解
- 引言
- 一、基础知识
- 1.1 什么是字符设备驱动
- 1.2 重要的数据结构
- read 函数
- write 函数
- open 函数
- release 函数
- 二、 驱动程序实现
- 2.1 完整的驱动代码示例
- 2.2 整体流程(图解)
- 2.3 用户空间与内核空间交互(图解)
- 2.4 驱动模块初始化
- 虚拟地址映射
- 2.5 拷贝数据
- 2.6 控制GPIO输出的LED开关状态
- 2.7 LED驱动程序的退出函数
- 三、实验过程
- 项目编译
- 连接开发板
- 挂载NFS文件系统
- 加载驱动(点灯!)
- 总结
引言
在Linux设备驱动开发中,字符设备驱动是最基础也是最常见的驱动类型。本文将从理论到实践,详细讲解字符设备驱动的开发流程,帮助读者掌握驱动开发的核心知识
一、基础知识
1.1 什么是字符设备驱动
字符设备(Character Device)是Linux中最基本的设备类型之一,它的特点是数据以字符流的方式被访问,像串口、键盘、LED等都属于字符设备。与块设备不同,字符设备不能随机访问,只能顺序读写。
1.2 重要的数据结构
在开发字符设备驱动之前,我们需要了解几个关键的数据结构:
struct file_operations {struct module *owner;ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);int (*open) (struct inode *, struct file *);int (*release) (struct inode *, struct file *);...
};
这个结构体定义了驱动程序需要实现的接口函数。
read 函数
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
作用: 响应用户空间的读取请求
参数:
- filp:文件结构体指针
- buf:用户空间缓冲区指针
- cnt:要读取的字节数
- offt:文件位置指针
返回值: - 成功返回实际读取的字节数
- 失败返回负错误码
- 使用场景:
- 读取设备状态
- 获取设备数据
- 将数据从内核空间复制到用户空间
write 函数
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
作用: 响应用户空间的写入请求
参数:
- filp:文件结构体指针
- buf:用户空间数据缓冲区指针
- cnt:要写入的字节数
- offt:文件位置指针
返回值: - 成功返回实际写入的字节数
- 失败返回负错误码
使用场景: - 向设备发送控制命令
- 更新设备状态
- 将数据从用户空间复制到内核空间
open 函数
static int led_open(struct inode *inode, struct file *filp)
作用: 当用户空间调用 open() 打开设备文件时被调用
参数:
- inode:包含文件系统信息的结构体,如设备号等
- filp:文件结构体,包含文件操作方法、私有数据等
返回值: - 成功返回0
- 失败返回负错误码
使用场景: - 初始化设备
- 检查设备状态
- 分配资源
- 增加使用计数
release 函数
static int led_release(struct inode *inode, struct file *filp)
作用: 当最后一个打开的文件被关闭时调用
参数:
- inode:索引节点结构体指针
- filp:文件结构体指针
返回值: - 成功返回0
- 失败返回负错误码
使用场景: - 释放资源
- 清理设备状态
- 减少使用计数
二、 驱动程序实现
2.1 完整的驱动代码示例
代码如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>#define DEV_MAJOR 0 /* 动态申请主设备号 */
#define DEV_NAME "red_led" /*led设备名字 *//* GPIO虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO04;
static void __iomem *SW_PAD_GPIO1_IO04;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;static int led_open(struct inode *inode, struct file *filp)
{return 0;
}static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{return -EFAULT;
}static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{unsigned char databuf[10];if (cnt > 10)cnt = 10;/*从用户空间拷贝数据到内核空间*/if (copy_from_user(databuf, buf, cnt)){return -EIO;}if (!memcmp(databuf, "on", 2)){iowrite32(0 << 4, GPIO1_DR);}else if (!memcmp(databuf, "off", 3)){iowrite32(1 << 4, GPIO1_DR);}/*写成功后,返回写入的字数*/return cnt;
}static int led_release(struct inode *inode, struct file *filp)
{return 0;
}/* 自定义led的file_operations 接口*/
static struct file_operations led_fops = {.owner = THIS_MODULE,.open = led_open,.read = led_read,.write = led_write,.release = led_release,
};int major = 0;
static int __init led_init(void)
{/* GPIO相关寄存器映射 */IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4);SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4);SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4);GPIO1_GDIR = ioremap(0x0209c004, 4);GPIO1_DR = ioremap(0x0209c000, 4);/* 使能GPIO1时钟 */iowrite32(0xffffffff, IMX6U_CCM_CCGR1);/* 设置GPIO1_IO04复用为普通GPIO*/iowrite32(5, SW_MUX_GPIO1_IO04);/*设置GPIO属性*/iowrite32(0x10B0, SW_PAD_GPIO1_IO04);/* 设置GPIO1_IO04为输出功能 */iowrite32(1 << 4, GPIO1_GDIR);/* LED输出高电平 */iowrite32(1 << 4, GPIO1_DR);/* 注册字符设备驱动 */major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops);printk(KERN_ALERT "led major:%d\n", major);return 0;
}static void __exit led_exit(void)
{/* 取消映射 */iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO04);iounmap(SW_PAD_GPIO1_IO04);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);/* 注销字符设备驱动 */unregister_chrdev(major, DEV_NAME);
}module_init(led_init);
module_exit(led_exit);MODULE_LICENSE("GPL2");
MODULE_AUTHOR("embedfire ");
MODULE_DESCRIPTION("led_module");
MODULE_ALIAS("led_module");
2.2 整体流程(图解)

2.3 用户空间与内核空间交互(图解)

2.4 驱动模块初始化
虚拟地址映射
- ioremap 函数
void __iomem *ioremap(unsigned long phys_addr, unsigned long size);
作用: 将物理地址映射到虚拟地址空间
参数:
- phys_addr:物理地址
- size:映射的大小(字节数)
返回值: 映射后的虚拟地址指针 - void * 类型的指针,指向被映射的虚拟地址
- __iomem 主要是用于编译器的检查地址在内核空间的有效性
为什么要用: Linux内核出于安全考虑,不允许直接访问物理地址,必须先映射到虚拟地址
GPIO相关寄存器映射
IMX6U_CCM_CCGR1 = ioremap(0x20c406c, 4);SW_MUX_GPIO1_IO04 = ioremap(0x20e006c, 4);SW_PAD_GPIO1_IO04 = ioremap(0x20e02f8, 4);GPIO1_GDIR = ioremap(0x0209c004, 4);GPIO1_DR = ioremap(0x0209c000, 4);
- 虚拟地址读写
void iowrite32(u32 b, void __iomem *addr) //写入一个双字(32bit)unsigned int ioread32(void __iomem *addr) //读取一个双字(32bit)
/* 使能GPIO1时钟 */iowrite32(0xffffffff, IMX6U_CCM_CCGR1);/* 设置GPIO1_IO04复用为普通GPIO*/iowrite32(5, SW_MUX_GPIO1_IO04);/*设置GPIO属性*/iowrite32(0x10B0, SW_PAD_GPIO1_IO04);/* 设置GPIO1_IO04为输出功能 */iowrite32(1 << 4, GPIO1_GDIR);/* LED输出高电平 */iowrite32(1 << 4, GPIO1_DR);
- register_chrdev 函数
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);
作用: 注册字符设备驱动
参数:
major:主设备号(0表示动态分配)
name:设备名称
fops:文件操作结构体
次设备号为0,次设备号数量为256
返回值: 成功返回主设备号,失败返回负值
为什么要用: 向Linux系统注册一个字符设备,使系统能够识别和管理该设备

2.5 拷贝数据
copy_from_user函数
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
作用: 将数据从用户空间复制到内核空间
参数:
- to:内核空间目标地址
- from:用户空间源地址
- n:复制的字节数
返回值: 成功返回0,失败返回未复制的字节数
为什么要用: 内核空间和用户空间是隔离的,需要专门的函数来安全地传输数据,要是有野指针会导致整个系统的崩溃,所以是起到一个安全的作用。
2.6 控制GPIO输出的LED开关状态
if (!memcmp(databuf, "on", 2)) // 比较是否接收到"on"命令
{iowrite32(0 << 4, GPIO1_DR); // GPIO1_4输出低电平,LED亮
}
else if (!memcmp(databuf, "off", 3)) // 比较是否接收到"off"命令
{iowrite32(1 << 4, GPIO1_DR); // GPIO1_4输出高电平,LED灭
}
- memcmp()函数:
int memcmp(const void *str1, const void *str2, size_t n)
// 比较两个内存区域的前n个字节
// 返回0表示相等
2.7 LED驱动程序的退出函数
static void __exit led_exit(void)
{// 1. 取消IO内存映射iounmap(IMX6U_CCM_CCGR1);iounmap(SW_MUX_GPIO1_IO04);iounmap(SW_PAD_GPIO1_IO04);iounmap(GPIO1_DR);iounmap(GPIO1_GDIR);// 2. 注销字符设备unregister_chrdev(major, DEV_NAME);
}
- iounmap 函数
void iounmap(void __iomem *addr);
作用: 解除I/O内存映射
参数:
addr: 要解除映射的虚拟地址
为什么要用: 释放ioremap占用的资源,防止内存泄漏
- unregister_chrdev 函数
void unregister_chrdev(unsigned int major, const char *name);
作用: 注销字符设备驱动
参数:
major:设备的主设备号
name:设备名称
为什么要用: 在驱动卸载时清理系统资源
三、实验过程
项目编译

然后make

连接开发板
打开手机热点并连上
让电脑跟手机在同一个局域网内
-
ubuntu端

-
开发板端

挂载NFS文件系统
sudo mount -t nfs ”NFS服务端IP”:/home/embedfire/workdir /mnt
我们ubuntu的IP为192.168.46.118
所以为
sudo mount -t nfs 192.168.46.118:/home/embedfire/workdir /mnt

挂载成功后进入共享文件夹查看
ubuntu把ko文件复制到共享文件夹中

我们到共享文件夹ls查看

加载驱动(点灯!)

244是设备号(动态分配)
0是次设备号
为什么是0
因为在register_chrdev函数定义了
次设备号在(0~256之间随便选)
ebf-buster-linux/include/linux/fs.h
static inline int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops)
{return __register_chrdev(major, 0, 256, name, fops);
}
创建设备文件

利用echo应用打开灯的命令


利用echo应用关灯的命令

卸载模块

总结
本文详细介绍了Linux字符设备驱动的开发流程,包括:
- 基本概念和原理
- 完整的代码实现
- 详细的流程图解
- 实际操作
通过本文的学习,大家应该能够掌握字符设备驱动的开发方法,并能够开发简单的字符设备驱动程序。
相关文章:
Linux驱动开发实战(一):LED控制驱动详解
Linux驱动开发野火实战(一):LED控制驱动详解 文章目录 Linux驱动开发野火实战(一):LED控制驱动详解引言一、基础知识1.1 什么是字符设备驱动1.2 重要的数据结构read 函数write 函数open 函数release 函数 二…...
PowerShell 执行策略:fnm管理软件安装nodejs无法运行npm,错误信息:about_Execution_Policies
通过fnm管理软件安装NodeJS后添加环境变量依然无法执行npm,提示无法加载文件,错误如下: PowerShell 执行策略简介: PowerShell 执行策略是一项安全功能,用于控制 PowerShell 加载配置文件和运行脚本的条件。 此功能有助于防止恶…...
论文阅读笔记:Deep Face Recognition: A Survey
论文阅读笔记:Deep Face Recognition: A Survey 1 介绍2 总览2.1 人脸识别组件2.1.1 人脸处理2.1.2 深度特征提取2.1.3 基于深度特征的人脸对比 3 网络结构和损失函数3.1 判别损失函数的演化3.1.1 基于欧式距离的损失3.1.2 基于角度/余弦边距的损失3.1.3 Softmax损失…...
本地dify绑定notion
需要用到 notion 的“集成”功能。对于个人用户来说,选择使用**内部 internal **集成,公司用户可以考虑使用公开 public 集成。 在下面的 notion 集成网站中申请一个新的集成: Notion – The all-in-one workspace for your notes, tasks, …...
k8s中pod的调度策略之pod的亲和性调度与反亲和性调度 一文搞懂 k8s中创建的pod如何调度?
接上文写的Node亲和性调度https://blog.csdn.net/soso678/article/details/144777397 Pod 间的亲和性和反亲和性(Affinity/AntiAffinity)调度 Pod 间亲和性与反亲和性使你可以基于已经在节点上运行的 Pod 的标签来约束 Pod 可以调度到的节点࿰…...
JSON Schema 入门指南:如何定义和验证 JSON 数据结构
文章目录 一、引言二、什么是 JSON Schema?三、JSON Schema 的基本结构3.1 基本关键字3.2 对象属性3.3 数组元素3.4 字符串约束3.5 数值约束 四、示例:定义一个简单的 JSON Schema五、使用 JSON Schema 进行验证六、实战效果6.1 如何使用 七、总结 一、引…...
借DeepSeek的风思考:未来可通过AI+视觉识别技术,实现足球比赛中的教练决策指挥系统,以此提高中国足球冲出亚洲夺取世界杯冠军
国家如此强大,必须需要提升国足冲向世界,一雪前耻的了。现在我脑洞大开,提出以下思路寄望国家重视。具体思考如下: 通过AI视觉识别技术实现足球比赛中的教练决策指挥系统,有潜力提升中国足球的竞争力。但要实现这一目标…...
DeepSeek今日连开3源!针对优化的并行策略,梁文锋本人参与开发
DeepSeek开源周第四天,直接痛快「1日3连发」,且全都围绕一个主题: 优化并行策略。 DualPipe:一种创新的双向流水线并行算法,能够完全重叠前向和后向计算-通信阶段,并减少“流水线气泡”。它通过对称的微批…...
【含文档+PPT+源码】基于过滤协同算法的旅游推荐管理系统设计与实现
项目介绍 本课程演示的是一款基于过滤协同算法的旅游推荐管理系统设计与实现,主要针对计算机相关专业的正在做毕设的学生与需要项目实战练习的 Java 学习者。 1.包含:项目源码、项目文档、数据库脚本、软件工具等所有资料 2.带你从零开始部署运行本套…...
蓝桥杯2024年第十五届省赛真题-传送阵
#include<stdio.h> #include<stdbool.h> #define MAX 100000 int circle[MAX];//记录每个环大小 int parent[MAX];//记录每个传送阵所属的环 int m[MAX]; bool visited[MAX]; int circleIndex0;//当前环的编号 //迭代实现换的查找void findcircle(int start){int c…...
数据存储:一文掌握存储数据到mysql的详细使用
文章目录 一、环境准备1.1 安装MySQL数据库1.2 安装Python MySQL驱动 二、连接到MySQL数据库三、执行基本的CRUD操作3.1 创建(Create):插入数据3.2 读取(Read):查询数据3.3 更新(Update…...
【华为OD机考】华为OD笔试真题解析(15)--异常的打卡记录
题目描述 考勤记录是分析和考核职工工作时间利用情况的原始依据,也是计算职工工资的原始依据,为了正确地计算职工工资和监督工资基金使用情况,公司决定对员工的手机打卡记录进行异常排查。 如果出现以下两种情况,则认为打卡异常…...
Java实战:使用HttpClient实现图片下载与本地保存
在当今数字化时代,网络资源的获取与处理已成为软件开发中的常见需求。其中,图片作为网络上最常见的资源之一,其下载与保存功能在许多应用场景中都显得尤为重要。无论是社交媒体平台、电商平台,还是个人项目,能够高效地…...
【实战】使用PCA可视化神经网络提取后的特征空间【附源码】
《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…...
DeepSeek05-大模型WebUI
一、说明: 将DeepSeek部署到前台Web界面的方法主要有以下几种推荐方案,涵盖开源工具、第三方客户端及特定场景适配方案: Open WebUIChatbox AICherry StudioSillyTavern 二、Open WebUI 安装配置教程 特点:Open WebUI 是一个开…...
第14天:C++异常处理实战指南 - 构建安全的文件解析系统
第14天:C异常处理实战指南 - 构建安全的文件解析系统 一、今日学习目标 🎯 掌握C异常处理的核心语法与流程🛡️ 理解RAII在资源管理中的关键作用📦 创建自定义文件解析异常体系🚀 实现安全的文件解析器原型 二、C异常…...
JavaScript遍历方式总结
目录 一、数组遍历方法 1.1for循环 1.2for...of循环 1.3forEach 1.4map方法 1.5filter方法 1.6reduce方法 1.7some方法 1.8every方法 二、对象遍历方法 2.1for...in方法 2.2values、keys方法 2.3entries方法 一、数组遍历方法 1.1for循环 最普通的循环…...
Python毕业设计选题:基于Python的社区爱心养老管理系统设计与实现_django
开发语言:Python框架:djangoPython版本:python3.7.7数据库:mysql 5.7数据库工具:Navicat11开发软件:PyCharm 系统展示 管理员登录 管理员功能界面 用户管理 身体健康界面 公共书籍界面 借阅信息界面 归还…...
Spring Boot整合WebSocket
目录 ?引言 1.WebSocket 基础知识 ?1.1 什么是 WebSocket? ?1.2 WebSocket 的应用场景 ?2.Spring Boot WebSocket 整合步骤 2.1 创建 Spring Boot 项目 2.2 添加 Maven 依赖 2.3 配置 WebSocket 2.4 创建 WebSocket 控制器 2.5 创建前端页面 引言 在…...
Pycharm使用matplotlib出现的问题(1、不能弹出图表 2、图表标题中文不显示)
Pycharm使用matplotlib出现的问题 问题1:Pycharm调试时出现:AttributeError: module backend_interagg has no attribute FigureCanvas. Did you mean: FigureCanvasAgg? 排查原因:可能是由于matplotlib后端设置不正确或与运行环境不兼容引…...
《宇树科技:解锁机器人技术的未来密码》:此文为AI自动生成
走进宇树科技 在科技飞速发展的今天,机器人领域正以前所未有的速度蓬勃发展,成为全球瞩目的焦点。在这个充满创新与挑战的领域中,宇树科技宛如一颗璀璨的明星,闪耀着独特的光芒。它不仅在国内机器人行业占据着重要地位,更是在国际舞台上崭露头角,成为了中国机器人技术的…...
Spark map与mapPartitions算子源码级深度解析
Spark map与mapPartitions算子源码级深度解析 一、核心源码结构差异 1. map算子实现逻辑 def map[U: ClassTag](f: T => U): RDD[U] = withScope {val cleanF = sc.clean(f)new MapPartitionsRDD[U, T](this, (context, pid, iter) => iter.map(cleanF)) }实现特征: …...
在 Vue 3 中,如何缓存和复用动态组件
在 Vue 3 中,如何缓存和复用动态组件,这有助于提高应用的性能,避免组件重复创建和销毁带来的开销。下面详细介绍其使用方法和相关配置。 1. 使用 <KeepAlive> 组件缓存动态组件 基本使用 <KeepAlive> 是 Vue 3 内置的一个组件…...
【PromptCoder】使用 package.json 生成 cursorrules
【PromptCoder】使用 package.json 生成 cursorrules 在当今快节奏的开发世界中,效率和准确性至关重要。开发者们不断寻找能够优化工作流程、帮助他们更快编写高质量代码的工具。Cursor 作为一款 AI 驱动的代码编辑器,正在彻底改变我们的编程方式。但如…...
给博客添加基于百度地图的足迹页面
使用百度地图 api 做的足迹页面一段时间了,经过一番改造,目前已基本能够满足自己需求。 一、添加百度地图 添加百度地图基本思路就是6点: 申请百度AK适当位置添加百度地图容器引入百度地图 api创建地图实例设置地图中心点初始化地图 这里…...
【构建工具】Gradle Kotlin DSL中的大小写陷阱:BuildConfigField
在Android开发当中,BuildConfig是一个非常有用的功能,它允许我们在构建过程中定义常量,并在运行时使用它们。But!!当我们从传统的Groovy DSL迁移到Kotlin DSL时或者被Android Studio坑的时候,有一些细微的差…...
南京来可电子CAN总线数据记录仪在汽车售后服务站的应用
南京来可电子CAN总线数据记录仪在汽车售后服务站的应用 南京来可电子(LaiCore)作为国内领先的车载数据采集设备供应商,其CAN总线数据记录仪凭借高精度、多协议兼容性及智能化功能,在汽车售后服务站中发挥重要作用。以下是其核心应…...
FreeSql + .Net6 多库连接实现
1、安装Nuget包 AutoMapper 2、program.cs里添加如下配置: services.AddSingleton(r >{var str configuration.GetConnectionString("MES");return new FreeSqlBuilder().UseConnectionString(DataType.SqlServer, str).Build<MESFlag>();});s…...
4个小时开发DeepSeek+baiduNaotu一键生成思维导图
一、引言 最近发现AI生成思维导图的解决方案普遍存在两个断层:用户需手动复制模型输出的JSON数据到脑图软件,且缺乏实时可视化反馈。基于日常使用的BaiduNaotu框架(其轻量级架构与简洁的UI设计已满足基础需求),我决定…...
(21)从strerror到strtok:解码C语言字符函数的“生存指南2”
❤个人主页:折枝寄北的博客 ❤专栏位置:简单入手C语言专栏 目录 前言1. 错误信息报告1.1 strerror 2. 字符操作2.1 字符分类函数2.2 字符转换函数 3. 内存操作函数3.1 memcpy3.2 memmove3.2memset3.3 memcmp 感谢您的阅读 前言 当你写下strcpy(dest, s…...
