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字符设备驱动的开发流程,包括:
- 基本概念和原理
- 完整的代码实现
- 详细的流程图解
- 实际操作
通过本文的学习,大家应该能够掌握字符设备驱动的开发方法,并能够开发简单的字符设备驱动程序。