文章目录
-
- 前言
- 一、字符设备驱动框架
-
- [1.1 字符设备驱动框架图解](#1.1 字符设备驱动框架图解)
- [1.2 字符设备驱动编写的步骤](#1.2 字符设备驱动编写的步骤)
- [1.3 相关函数接口](#1.3 相关函数接口)
- 二、编写LED灯驱动流程
-
- [2.1 原理图](#2.1 原理图)
- [2.2 相关寄存器](#2.2 相关寄存器)
- [2.3 内存映射函数接口](#2.3 内存映射函数接口)
- [2.4 寄存器的读写接口](#2.4 寄存器的读写接口)
- 三、逐步编写LED灯驱动
-
- [3.1 构建驱动工程框架](#3.1 构建驱动工程框架)
- [3.2 申请注册设备号](#3.2 申请注册设备号)
- [3.3 初始化字符设备结构体](#3.3 初始化字符设备结构体)
- [3.4 LED寄存器配置](#3.4 LED寄存器配置)
- [3.5 LED操作函数完善](#3.5 LED操作函数完善)
- [3.6 应用层主函数](#3.6 应用层主函数)
- [3.7 交互界面操作](#3.7 交互界面操作)

前言
在上篇文章【Linux驱动开发】驱动介绍及驱动工程搭建中,我们学习了Linux驱动的分类,还完成了对驱动工程构建的具体了解与具体实现,在这篇文章中我们将实现字符设备LED的驱动编写,从51的点灯大师到IMX6ULL裸机的点灯大师,到现在即将成为Linux驱动的点灯大师了。
一、字符设备驱动框架
1.1 字符设备驱动框架图解

从上面的图中可以看出,其实都在围绕三个数据结构转:

cdev结构体
字符设备内核对象,内核里代表一个字符设备实例。保存设备号dev_t和操作方法集合file_operations;通过cdev_add()向内核注册、cdev_del()注销,是字符设备驱动的核心枢纽。struct file_operations文件操作方法表
函数指针集合,存放open/read/write/release/ioctl等回调函数。应用层open/read/write系统调用,经过 VFS(虚拟文件系统) 最终会调用这里注册的驱动函数;驱动只需要实现自己用到的接口即可,不需要全部填充。dev_t设备号类型(不是结构体,是数据类型)
存放主设备号 + 次设备号。
理解这三个数据结构,字符设备驱动编写就更容易理解了。
1.2 字符设备驱动编写的步骤
- 申请设备号,主次设备号 MKDEV, alloc_chrdev_region
- 注册设备号 register_chrdev_region , alloc_chrdev_region
- 初始化字符设备结构体 cdev_init(自动关联fops), cdev_add(驱动注册到内核),cdev_alloc (手动关联)
- 准备 file_operation , 注册 应用层对应的open,close,read, write函数。给函数指针赋值
1.3 相关函数接口
-
cdev操作函数接口
函数名 功能 cdev_init 初始化字符设备结构体 cdev_alloc 申请字符设备结构体 cdev_add 添加字符设备 cdev_del 删除字符设备 -
字符设备操作接口
函数名 功能 register_chrdev_region 静态注册已知设备号范围(需提前确定主设备号) alloc_chrdev_region 动态分配设备号,并注册(内核自动分配主设备号) unregister_chrdev_region 释放注册的设备号 register_chrdev 注册设备号并关联file_operations结构体(早期内核使用,2.6版本以后尽量不使用) unregister_chrdev 注销注册的设备号(早期内核使用,2.6版本以后尽量不使用) 宏 作用 输入 输出 MKDEV(ma, mi) 将主设备号、次设备号合并生成dev_t设备号 ma:主设备号;mi:次设备号 dev_t 组合设备号 MAJOR(dev) 从dev_t中提取主设备号 dev_t类型设备号 int 主设备号 MINOR(dev) 从dev_t中提取次设备号 dev_t类型设备号 int 次设备号 当创建设备类型后,进入/proc/devices目录下会发现没有具体的设备 。需要我们手动创建节点:
mknod /dev/led0 c 248 0 -
file_operation 结构体

二、编写LED灯驱动流程
2.1 原理图
根据字符设备驱动流程,编写LED灯驱动,实现应用程序控制LED灯亮灭。
在IMX6ULL这块开发板上LED的原理图如下:


低电平亮,高电平灭。引脚为GPIO1_03。
2.2 相关寄存器
在裸机阶段,我们已经实现过LED灯的相关操作,其主要涉及到的寄存器如下:
-
IO引脚复用寄存器
-
IO引脚电气特性
-
GPIO 方向
-
gpio 的数据寄存器
-
时钟寄存器 ,不需要设置,裸机需要设置
Linux 内核不能直接操作物理地址 !内核开启 MMU,使用虚拟地址。
需要调用 ioremap(),把上面的物理地址映射成内核虚拟地址,驱动代码操作映射后的虚拟地址。因此我们还需要找到这些寄存器的物理地址:
| 寄存器 | 名称 | 地址 |
|---|---|---|
| IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 | 引脚复用功能 | 0x20E0068 |
| IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 | 电气特性 | 0x20E02F4 |
| GPIO1_DR | 数据寄存器 | 0x209C000 |
| GPIO1_GDIR | 方向 | 0x209C004 |
2.3 内存映射函数接口
| 函数名 | 功能 |
|---|---|
| ioremap | 将内核虚拟地址映射到物理地址 //#include <linux/io.h> #include <asm/io.h> |
| iounmap | 解除映射的虚拟地址 |
| devm_ioremap | 将物理地址映射到内存虚拟地址空间(驱动卸载时自动解除映射) |
| devm_iounmap | 提前解除映射关系 |
| devm_ioremap_resource | 整合资源申请并映射,避免硬件冲突,适用设备树场景 |
| copy_to_user | 内核层向用户层拷贝数据 |
| copy_from_user | 从用户层拷贝数据到内核层 |
2.4 寄存器的读写接口
| 函数名 | 功能 |
|---|---|
| readb(regaddr) | 读寄存器(regaddr地址)1个字节 u8 |
| readw(regaddr) | 读寄存器2个字节 u16 |
| readl(regaddr) | 读寄存器4个字节 u32 |
| writeb(v,c) | 写寄存器1个字节,(value,regaddr) |
| writew(v,c) | 写寄存器2个字节,(value,regaddr) |
| writel(v,c) | 写寄存器4个字节,(value,regaddr) |
三、逐步编写LED灯驱动
3.1 构建驱动工程框架
将上篇文章中构建的驱动工程框架可以搬过来,记得修改相关的名字。
3.2 申请注册设备号
设备号的获得有两种方式:
-
静态注册已知设备号范围(需提前确定主设备号)
MKDEV+register_chrdev_region -
动态分配设备号,并注册(内核自动分配主设备号)
alloc_chrdev_region在本篇文章中就采用第二中,使得编写更容易。
若使用
alloc_chrdev_region时,vscode没有自动补齐添加头文件时,可以使用vi -t '函数名'来查询函数接口所在的文件。

参数 说明 dev_t *dev输出参数 。函数成功后,将拼接好的 dev_t(主+次设备号)写入该地址。 外部使用MAJOR(*dev)获取主设备号,MINOR(*dev)获取起始次设备号。unsigned baseminor起始次设备号,申请次设备号区间的第一个编号,一般填 0。unsigned count需要连续占用的次设备号数量。 例: baseminor=0, count=4→ 占用次设备号0,1,2,3。const char *name设备名称,会显示在 /proc/devices,用于查看设备号。在了解函数后,我们就可以这样写:


3.3 初始化字符设备结构体
-
初始化字符设备结构体 cdev_init(自动关联fops), cdev_add(驱动注册到内核)
这里已经需要fops与cdev结构体了,我们先将其定义出来:c//设备结构体 static struct cdev cdev; //对自己的操作函数先进行隐式声明,使其与fops关联 static int myopen(struct inode *node, struct file *f); static int myclose(struct inode *node, struct file *f); static ssize_t myread(struct file *f, char __user *user, size_t size, loff_t *loff); static ssize_t mywrite(struct file *f, const char __user *user, size_t size, loff_t *loff); //定义文件操作结构体,选取自己需要的操作 static struct file_operations fops = { .owner = THIS_MODULE, .open = myopen, .release = myclose, .read = myread, .write = mywrite, }; static int myopen(struct inode *node, struct file *f) { return 0; } ......然后初始化字符设备结构体:
c//设备结构体初始化,关联fops文件操作结构体 cdev_init(&cdev, &fops); //注册设备到内核 ret = cdev_add(&cdev, dev_no, 1); if (ret < 0) { pr_err("cdev_add error\n"); return ret; }
到此led这个设备在Linux中已经有了,接下来就是对LED灯的相关操作进行配置了。
3.4 LED寄存器配置
寄存器配置:
c
io_mux = ioremap(IO_MUX, 4);
io_pad = ioremap(IO_PAD, 4);
io_data = ioremap(IO_DATA, 4);
io_dir = ioremap(IO_DIR, 4);
//设置GPIO复用寄存器
v = readl(io_mux);
v = v | 0x05;
writel(v, io_mux);
//设置GPIO电气寄存器
v = readl(io_pad);
v = v | 0x10b0;
writel(v, io_pad);
//设置GPIO数据
v = readl(io_data);
v = v | (1 << 3);
writel(v, io_data);
//设置输入输出方向
v = readl(io_dir);
v = v | (1 << 3);
writel(v, io_dir);
3.5 LED操作函数完善
c
static ssize_t mywrite(struct file *f, const char __user *data, size_t size, loff_t *loff)
{
unsigned long ret = 0;
int led_state = 0;
u32 v = 0;
ret = copy_from_user(&led_state, data, 4);
if (0 == led_state)
{
// led off
v = readl(io_data);
v = v | (1 << 3);
writel(v, io_data);
}
else if (1 == led_state)
{
// led on
v = readl(io_data);
v = v & ~(1 << 3);
writel(v, io_data);
}
return size;
}
c
static int myclose(struct inode *node, struct file *f)
{
iounmap(io_dir);
iounmap(io_data);
iounmap(io_pad);
iounmap(io_mux);
return 0;
}
3.6 应用层主函数
实现led灯1秒亮灭:
c
int main(void)
{
int fd = open("/dev/led0", O_RDWR);
if (-1 == fd)
{
perror("open error");
return -1;
}
int on = 0;
while (1)
{
write(fd, &on, sizeof(on));
sleep(1);
if (0 == on)
{
on = 1;
}
else if (1 == on)
{
on = 0;
}
}
return 0;
}
3.7 交互界面操作

此时,会发现led没有按照应用层写的代码一秒闪烁,原因是在设备树中有一个用来表示设备运行的函数占用了此LED,因此,我们需要在arch/arm/boot/dts/imx6ull-alientek-emmc.dts中, 注释dtsleds,然后重新编译dts。

这样再次重复操作后就会发现led开始正常工作了。
当然,目前的这个led驱动还不完整,比如说,若设中途出现错误,会直接返回,这样之前申请的一些空间就没有释放,会造成内存资源泄漏,然后还有手动创建节点很麻烦,有没有自动创建节点的方法,答案是当然有,这些都会在后面一篇主题为sys文件系统与misc的文章中提到。