目录
一、重温字符设备驱动开发步骤
①本贴的目的
紧接着上一个帖子的内容:嵌入式Linux_字符设备驱动开发流程_最简单|最完整|最入门|零基础|保姆级|驱动开发案例_LED_(通过TFTP启动Linux内核和NFS挂载根文件系统+LED驱动编写+亮灭应用测试)
我们将在本贴:
根据字符设备驱动开发的步骤,一点一点的详解我们编写的驱动代码。
②重温字符设备驱动开发编写步骤
Ⅰ、字符设备驱动开发程序编写步骤:
一、定义字符设备结构体(包含当前设备信息)
二、定义文件操作函数及其结构体
①定义文件操作函数
②定义文件操作函数结构体
三、注册字符设备(模块入口部分函数)
①分配设备号
a、静态分配设备号(主动定义)
b、动态分配设备号(申请自动分配)
②初始化字符设备(初始化字符设备结构体并绑定文件操作函数)
③注册字符设备到内核
④创建字符设备类
⑤在该类下创建字符设备节点
四、注销字符设备部分(模块出口部分函数)
①从内核中删除字符设备
②注销设备号
③销毁设备节点
④销毁设备类
五、模块的入口出口函数绑定
①module_init(模块入口部分函数)
②module_exit(模块出口部分函数)
六、结尾处添加许可证|作者信息|描述|版本信息
③重温测试APP编写步骤
Ⅱ、测试APP程序编写步骤:
一、检查参数数量是否正确
二、获取设备文件名
三、打开设备文件
四、将控制命令写入设备文件
五、关闭设备文件
④重温字符设备从编写到使用的过程
完整的测试过程:
Ⅰ、字符设备驱动开发程序编写步骤
Ⅱ、测试APP程序编写步骤
Ⅲ、编写Makefile编译驱动和APP
Ⅳ、在开发板中加载驱动
Ⅴ、使用APP测试驱动功能
Ⅵ、卸载驱动
二、代码详解
①驱动代码的完整呈现
【led.c】
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/**********************************************************
Copyright © WangYongyang Co., Ltd. All rights reserved.
文件名: newchrled.c
作者: 汪永阳
版本: v1.0
描述: 字符设备驱动开发__点亮LED(来源:正点原子)
日志: v1.0 2024/5/27 汪永阳创建
************************************************************/
#define LED_WYY_CNT 1 /* 设备号个数 */
#define LED_WYY_NAME "led_wyy"
#define LEDOFF 0
#define LEDON 1
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
struct led_wyy_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct led_wyy_dev led_wyy;
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val|= (1 << 3);
writel(val, GPIO1_DR);
}
}
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &led_wyy;
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0];
if(ledstat == LEDON) {
led_switch(LEDON);
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF);
}
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations led_wyy_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
static int __init led_init(void)
{
u32 val = 0;
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, IMX6U_CCM_CCGR1);
writel(5, SW_MUX_GPIO1_IO03);
writel(0x10B0, SW_PAD_GPIO1_IO03);
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
if (led_wyy.major) {
led_wyy.devid = MKDEV(led_wyy.major, 0);
register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
} else {
alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
led_wyy.major = MAJOR(led_wyy.devid);
led_wyy.minor = MINOR(led_wyy.devid);
}
printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);
led_wyy.cdev.owner = THIS_MODULE;
cdev_init(&led_wyy.cdev, &led_wyy_fops);
cdev_add(&led_wyy.cdev, led_wyy.devid, LED_WYY_CNT);
led_wyy.class = class_create(THIS_MODULE, LED_WYY_NAME);
if (IS_ERR(led_wyy.class)) {
return PTR_ERR(led_wyy.class);
}
led_wyy.device = device_create(led_wyy.class, NULL, led_wyy.devid, NULL, LED_WYY_NAME);
if (IS_ERR(led_wyy.device)) {
return PTR_ERR(led_wyy.device);
}
return 0;
}
static void __exit led_exit(void)
{
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
cdev_del(&led_wyy.cdev);
unregister_chrdev_region(led_wyy.devid, LED_WYY_CNT);
device_destroy(led_wyy.class, led_wyy.devid);
class_destroy(led_wyy.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wangyongyang");
MODULE_DESCRIPTION("A simple Linux char driver");
MODULE_VERSION("1.0");
②驱动代码详解
-----------------------------【包含的头文件:】-----------------------------
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/types.h>
:定义了内核中常用的基本数据类型,如 u8、u32 等无符号整数类型和 s8、s32 等有符号整数类型。
#include <linux/kernel.h>
:提供了许多核心内核函数和宏定义,例如 printk(内核打印函数)、container_of(获取包含某成员变量的结构体指针)等。
#include <linux/delay.h>
:提供了内核中的延迟函数,如 msleep(毫秒级延迟)、udelay(微秒级延迟)等。
#include <linux/ide.h>
:主要包含了与 IDE 硬盘接口相关的函数和数据结构。
#include <linux/init.h>
:定义了模块初始化和退出的宏,如 module_init 和 module_exit,这些宏用于指定模块的初始化函数和退出函数。
#include <linux/module.h>
:提供了模块编程的基础设施,定义了模块的宏、函数和数据结构,如 MODULE_LICENSE、MODULE_AUTHOR 等。
#include <linux/errno.h>
:定义了错误代码,例如 -EFAULT、-ENOMEM 等,用于表示各种错误类型。
#include <linux/gpio.h>
:提供了与 GPIO(通用输入输出)相关的接口和函数,如 gpio_request、gpio_direction_output 等,用于配置和操作 GPIO 引脚。
#include <linux/cdev.h>
:提供了字符设备的接口和数据结构,如 struct cdev 和相关的函数 cdev_init、cdev_add、cdev_del 等。
#include <linux/device.h>
:提供了设备模型相关的函数和数据结构,如 struct class 和 struct device,用于设备的创建和管理。
#include <asm/mach/map.h>
:提供了内存映射相关的宏和函数,如 ioremap、iounmap 等,用于将物理地址映射到虚拟地址空间。
#include <asm/uaccess.h>
:提供了用户空间与内核空间之间的数据拷贝函数,如 copy_from_user、copy_to_user 等。
#include <asm/io.h>
:提供了 I/O 端口操作的函数和宏,用于读写 I/O 端口和内存映射的 I/O 寄存器,如 readl、writel 等。
----------------------------【宏定义】----------------------------
#define LED_WYY_CNT 1
#define LED_WYY_NAME "led_wyy"
#define LEDOFF 0
#define LEDON 1
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
这些宏定义了设备的基本信息和用于控制LED的寄存器地址。
#define LED_WYY_CNT 1
字符设备的数量
#define LED_WYY_NAME "led_wyy"
字符设备的命名
#define LEDOFF 0
关灯
#define LEDON 1
开灯
#define CCM_CCGR1_BASE (0X020C406C)
控制时钟的寄存器基地址
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
设置GPIO复用的寄存器基地址
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
设置GPIO功能的寄存器基地址
#define GPIO1_DR_BASE (0X0209C000)
设置GPIO高低电平的寄存器基地址
#define GPIO1_GDIR_BASE (0X0209C004)
设置GPIO输入还是输出的寄存器基地址
【经过内存映射以后的寄存器地址指针】
这里要对地址映射进行一个简单的说明:
Linux驱动开发也可以像裸机开发一样操作寄存器,但Linux不能直接对寄存器物理地址进行读写操作。
在Linux里面操作的都是虚拟地址,所以需要先将物理地址映射到虚拟地址。
实现这一功能的,就是MMU。
MMU(Memory Management Unit,内存管理单元)是一种硬件模块,用于在CPU和内存之间实现虚拟内存管理。其主要功能是将虚拟地址转换为物理地址,同时提供访问权限的控制和缓存管理等功能。(传统32不能跑linux系统就是因为没有mmu)
获得物理物理地址对应的虚拟地址就是使用ioremap函数。
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;
这些指针会在驱动程序初始化时通过 ioremap 函数进行映射:
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
----------------------------【定义字符设备结构体】----------------------------
struct led_wyy_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
struct led_wyy_dev led_wyy;
----------------------------【LED控制函数】----------------------------
void led_switch(u8 sta)
{
u32 val = 0;
if(sta == LEDON) {
val = readl(GPIO1_DR);
val &= ~(1 << 3);
writel(val, GPIO1_DR);
}else if(sta == LEDOFF) {
val = readl(GPIO1_DR);
val|= (1 << 3);
writel(val, GPIO1_DR);
}
}
val = readl(GPIO1_DR);
从 GPIO1_DR 寄存器读取当前的值,存储在 val 中。
val &= ~(1 << 3);
使用按位或操作设置 val 中第3位(对应于GPIO1_IO03),这意味着将对应引脚设置为高电平,通常表示LED灭。
writel(val, GPIO1_DR);
writel 函数是用于写入I/O内存地址的一个宏或内联函数。它通常包含在内核头文件 <asm/io.h> 中。它用于向特定的内存地址写入一个32位的值,通常用于操作硬件寄存器。
----------------------------【定义文件操作函数及其结构体】----------------------------
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &led_wyy;
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if(retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0];
if(ledstat == LEDON) {
led_switch(LEDON);
} else if(ledstat == LEDOFF) {
led_switch(LEDOFF);
}
return 0;
}
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
static struct file_operations led_wyy_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
解析:
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &led_wyy;
return 0;
}
文件操作函数:打开
参数:
inode:指向文件的 inode 结构的指针。
filp:指向文件结构的指针。
作用:led_open 函数在用户进程打开设备文件时被调用。
filp->private_data = &led_wyy;
它将设备的私有数据结构 led_wyy 赋值给文件结构的 private_data 成员,这样在后续的操作(如读、写)中可以访问该设备的私有数据。
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
return 0;
}
文件操作函数:读
参数:
filp:指向文件结构的指针。
buf:指向用户空间缓冲区的指针。
cnt:要读取的字节数。
offt:文件偏移量指针。
作用:led_read 函数在用户进程读取设备文件时被调用。此实现只是返回 0,表示没有数据可读。通常会将设备的状态信息复制到用户空间,这里什么也没操作,因为这里只有点灯,没有读取的数据。
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue;
unsigned char databuf[1];
unsigned char ledstat;
retvalue = copy_from_user(databuf, buf, cnt);
if (retvalue < 0) {
printk("kernel write failed!\r\n");
return -EFAULT;
}
ledstat = databuf[0];
if (ledstat == LEDON) {
led_switch(LEDON);
} else if (ledstat == LEDOFF) {
led_switch(LEDOFF);
}
return 0;
}
文件操作函数:写
参数:
filp:指向文件结构的指针。
buf:指向用户空间缓冲区的指针。
cnt:要写入的字节数。
offt:文件偏移量指针。
作用:led_write 函数在用户进程向设备文件写入数据时被调用。它将用户空间的数据复制到内核空间的 databuf 缓冲区中,并根据数据决定是打开还是关闭LED。
copy_from_user(databuf, buf, cnt);
将用户空间的数据复制到内核空间。
根据 databuf[0] 的值,调用 led_switch 函数来打开或关闭LED。
return 0:
写操作成功,返回 0。
static int led_release(struct inode *inode, struct file *filp)
{
return 0;
}
文件操作函数:释放
参数:
inode:指向文件的 inode 结构的指针。
filp:指向文件结构的指针。
作用:led_release 函数在用户进程关闭设备文件时被调用。此实现不执行任何操作,只是返回 0。通常可以在这里释放资源。
static struct file_operations led_wyy_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
led_wyy_fops 结构体定义了操作设备文件的接口函数,这些函数会在用户进程对设备文件进行相应操作时被调用。
----------------------------【注册字符设备(模块入口部分函数)】----------------------------
static int __init led_init(void)
{
u32 val = 0;
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26);
val |= (3 << 26);
writel(val, IMX6U_CCM_CCGR1);
writel(5, SW_MUX_GPIO1_IO03);
writel(0x10B0, SW_PAD_GPIO1_IO03);
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
if (led_wyy.major) {
led_wyy.devid = MKDEV(led_wyy.major, 0);
register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
} else {
alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
led_wyy.major = MAJOR(led_wyy.devid);
led_wyy.minor = MINOR(led_wyy.devid);
}
printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);
led_wyy.cdev.owner = THIS_MODULE;
cdev_init(&led_wyy.cdev, &led_wyy_fops);
cdev_add(&led_wyy.cdev, led_wyy.devid, LED_WYY_CNT);
led_wyy.class = class_create(THIS_MODULE, LED_WYY_NAME);
if (IS_ERR(led_wyy.class)) {
return PTR_ERR(led_wyy.class);
}
led_wyy.device = device_create(led_wyy.class, NULL, led_wyy.devid, NULL, LED_WYY_NAME);
if (IS_ERR(led_wyy.device)) {
return PTR_ERR(led_wyy.device);
}
return 0;
}
解析:
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);
将物理地址映射到内核虚拟地址空间,前面说过的。
val = readl(IMX6U_CCM_CCGR1);
val &= ~(3 << 26); // 清除第26位和27位
val |= (3 << 26); // 设置第26位和27位为1,使能GPIO1的时钟
writel(val, IMX6U_CCM_CCGR1);
val = readl(IMX6U_CCM_CCGR1);
从寄存器的地址读回原本寄存器值
val &= ~(3 << 26);
第26位和27位置0
val |= (3 << 26);
设置第26位和27位为1,使能GPIO1的时钟
writel(val, IMX6U_CCM_CCGR1);
将改变后的寄存器的值写回寄存器的地址
writel(5, SW_MUX_GPIO1_IO03);
复用为GPIO1_IO03。
writel(0x10B0, SW_PAD_GPIO1_IO03);
/*寄存器SW_PAD_GPIO1_IO03设置IO属性
*bit 16:0 HYS关闭
*bit [15:14]: 00 默认下拉
*bit [13]: 0 kepper功能
*bit [12]: 1 pull/keeper使能
*bit [11]: 0 关闭开路输出
*bit [7:6]: 10 速度100Mhz
*bit [5:3]: 110 R0/6驱动能力
*bit [0]: 0 低转换率
*/
val = readl(GPIO1_GDIR);
val &= ~(1 << 3);
val |= (1 << 3);
writel(val, GPIO1_GDIR);
类似的寄存器操作,配置GPIO1_IO03引脚为输出。
val = readl(GPIO1_DR);
val |= (1 << 3);
writel(val, GPIO1_DR);
设置GPIO1_IO03引脚输出高电平,默认关闭LED。
if (led_wyy.major) {
led_wyy.devid = MKDEV(led_wyy.major, 0);
register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
} else {
alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
led_wyy.major = MAJOR(led_wyy.devid);
led_wyy.minor = MINOR(led_wyy.devid);
}
printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);
----------------------------【分配设备号】----------------------------
首先,根据是否已定义主设备号来决定分配设备号的方法。(推荐使用动态分配法)
分支1:已定义主设备号
if (led_wyy.major) {
led_wyy.devid = MKDEV(led_wyy.major, 0);
register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME);
}
led_wyy.major:
表示主设备号。如果这个值不为0,说明主设备号已经定义。
MKDEV(led_wyy.major, 0):
使用宏MKDEV将主设备号和次设备号(0)组合成设备号devid。
register_chrdev_region(led_wyy.devid, LED_WYY_CNT, LED_WYY_NAME):
注册设备号区域。
LED_WYY_CNT
表示要注册的连续设备号的数量,这里为1。
LED_WYY_NAME
是设备的名称。
分支2:未定义主设备号
} else {
alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME);
led_wyy.major = MAJOR(led_wyy.devid);
led_wyy.minor = MINOR(led_wyy.devid);
}
alloc_chrdev_region(&led_wyy.devid, 0, LED_WYY_CNT, LED_WYY_NAME):
动态分配设备号,存储在led_wyy.devid中。这里0表示次设备号起始值,LED_WYY_CNT表示要分配的设备号数量,LED_WYY_NAME是设备的名称。
led_wyy.major = MAJOR(led_wyy.devid):
从分配到的设备号中提取主设备号并赋值给led_wyy.major。
led_wyy.minor = MINOR(led_wyy.devid):
从分配到的设备号中提取次设备号并赋值给led_wyy.minor。
printk("led_wyy major=%d,minor=%d\r\n",led_wyy.major, led_wyy.minor);
分配好设备号之后,打印主次设备号信息。
----------------------------【初始化字符设备结构体并绑定文件操作函数】----------------------------
led_wyy.cdev.owner = THIS_MODULE;
cdev_init(&led_wyy.cdev, &led_wyy_fops);
设置设备的所有者:
led_wyy.cdev.owner = THIS_MODULE;:
将字符设备的所有者设置为当前内核模块,以便内核可以管理模块的引用计数,防止模块在设备使用期间被卸载。
初始化字符设备:
cdev_init(&led_wyy.cdev, &led_wyy_fops);:
初始化字符设备结构体,并将文件操作函数 led_wyy_fops 与字符设备关联起来,使得对该字符设备的文件操作会调用相应的函数(如 open、read、write、release 等)。
----------------------------【注册字符设备到内核】----------------------------
cdev_add(&led_wyy.cdev, led_wyy.devid, LED_WYY_CNT);
cdev_add
函数将 cdev 结构体注册到内核的字符设备子系统,使得该字符设备可以被用户空间访问。成功调用 cdev_add 后,用户空间程序可以通过设备文件(如 /dev/led)访问该字符设备,并触发相应的文件操作函数(如 open、read、write、release 等)。
----------------------------【创建字符设备类】----------------------------
led_wyy.class = class_create(THIS_MODULE, LED_WYY_NAME);
if (IS_ERR(led_wyy.class)) {
return PTR_ERR(led_wyy.class);
}
创建设备类:
class_create
函数创建一个设备类 LED_WYY_NAME,并将其指针赋值给 led_wyy.class。
检查是否创建成功:
IS_ERR
宏检查 led_wyy.class 是否是错误指针。
如果是错误指针,表示类创建失败,使用 PTR_ERR 提取错误码并返回。
----------------------------【在该类下创建设备节点】----------------------------
led_wyy.device = device_create(led_wyy.class, NULL, led_wyy.devid, NULL, LED_WYY_NAME);
if (IS_ERR(led_wyy.device)) {
return PTR_ERR(led_wyy.device);
}
创建设备:
device_create
函数创建一个设备节点,并将其指针赋值给 led_wyy.device。
检查是否创建成功:
IS_ERR
宏检查 led_wyy.device 是否是错误指针。
如果是错误指针,表示设备创建失败,使用 PTR_ERR 提取错误码并返回。
----------------------------【注销字符设备部分(模块出口部分函数)】----------------------------
static void __exit led_exit(void)
{
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
cdev_del(&led_wyy.cdev);
unregister_chrdev_region(led_wyy.devid, LED_WYY_CNT);
device_destroy(led_wyy.class, led_wyy.devid);
class_destroy(led_wyy.class);
}
解析:
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
iounmap
用于解除对先前通过 ioremap 获取的 I/O 内存区域的映射。
cdev_del(&led_wyy.cdev);
调用 cdev_del 删除字符设备结构体 led_wyy.cdev。
unregister_chrdev_region(led_wyy.devid, LED_WYY_CNT);
释放先前分配的设备号 led_wyy.devid。
device_destroy(led_wyy.class, led_wyy.devid);
调用 device_destroy 销毁通过 device_create 创建的设备节点。
class_destroy(led_wyy.class);
调用 class_destroy 销毁通过 class_create 创建的设备类。
----------------------------【模块的入口出口函数绑定】----------------------------
module_init(led_init);
module_exit(led_exit);
初始化和清理:
module_init(led_init);:
当模块加载时,调用 led_init 函数。
module_exit(led_exit);:
当模块卸载时,调用 led_exit 函数。
----------------------------【许可证、作者、描述、版本】----------------------------
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wangyongyang");
MODULE_DESCRIPTION("A simple Linux char driver");
MODULE_VERSION("1.0");
MODULE_LICENSE("GPL");
指定模块使用 GPL 许可证。
MODULE_AUTHOR("wangyongyang");
指定模块的作者为 wangyongyang。
MODULE_DESCRIPTION("A simple Linux char driver");
描述模块为一个简单的 Linux 字符设备驱动程序。
MODULE_VERSION("1.0");
指定模块的版本为 1.0。
③用户测试APP代码的完整呈现
【ledapp.c】
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/**********************************************************
Copyright © WangYongyang Co., Ltd. All rights reserved.
文件名: xxx
作者: 汪永阳
版本: v1.0
描述: LED_APP_test
日志: v1.0 2024/5/27 汪永阳创建
************************************************************/
#define LEDOFF 0
#define LEDON 1
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]);
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
retvalue = close(fd);
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
④用户测试APP代码详解
----------------------------【头文件】----------------------------
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
stdio.h:标准输入输出函数,如 printf。
unistd.h:POSIX 操作系统 API 函数,如 close、write。
sys/types.h 和 sys/stat.h:文件系统数据类型和操作函数。
fcntl.h:文件控制函数,如 open。
stdlib.h:标准库函数,如 atoi。
string.h:字符串操作函数。
----------------------------【检查参数数量是否正确】----------------------------
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
检查命令行参数的数量是否正确。如果参数数量不为 3,打印错误信息并退出程序。
----------------------------【获取设备文件名】----------------------------
filename = argv[1];
从命令行参数获取设备文件名。
----------------------------【打开设备文件】----------------------------
fd = open(filename, O_RDWR);
if(fd < 0){
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
尝试以读写模式打开指定的设备文件。如果打开失败,打印错误信息并退出程序。
----------------------------【读取命令参数】----------------------------
databuf[0] = atoi(argv[2]);
将命令行参数(打开或关闭命令)转换为整数,并存储在 databuf 中。
----------------------------【将控制命令写入设备文件】----------------------------
retvalue = write(fd, databuf, sizeof(databuf));
if(retvalue < 0){
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
将 databuf 中的值写入设备文件,以控制 LED 的状态。如果写入失败,打印错误信息并关闭文件。
----------------------------【关闭设备文件】----------------------------
retvalue = close(fd);
if(retvalue < 0){
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
三、使用方法及其响应过程
./ledapp /dev/led_wyy 1 # 打开LED
./ledapp /dev/led_wyy 0 # 关闭LED
./ledapp /dev/led_wyy 1
执行ledapp文件,打开设备文件/dev/led_wyy,并写入数据1.
在这个例子中,/dev/led_wyy
是一个设备文件,而非普通文件。设备文件通常代表外设(如硬件设备),其操作会被内核中的设备驱动程序截获并处理:
/dev/led_wyy
与字符设备驱动led_wyy关联,该驱动实现了文件操作接口(open、read、write、close 等)。
例如,当 open
被调用时,内核会调用相应的设备驱动的 led_open
函数。
具体的设备驱动关联:
在驱动代码中,字符设备驱动通过 file_operations 结构体定义了对应的操作:
static struct file_operations led_wyy_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
打开设备文件/dev/led_wyy—即应用程序调用 open("/dev/led_wyy", O_RDWR)
时,内核会调用 led_open 函数进行设备文件的打开操作。
写入数据1即写操作函数write(fd, databuf, sizeof(databuf))
调用驱动程序的 led_write 函数:
./ledapp /dev/led_wyy 1
通过向设备文件写入数据1,调用驱动程序的 led_write 方法,实现对 LED 状态的控制。
copy_from_user(databuf, buf, cnt)
将用户空间的数据拷贝到内核空间。
根据 databuf[0]
的值(1)
,调用 led_switch(LEDON)
打开 LED。
欢迎评论区纠错和讨论问题。