嵌入式Linux驱动开发(2):字符设备LED驱动
Linux 下 LED 灯驱动原理
I.MX6U-ALPHA 开发板上的 LED 连接到 I.MX6ULL 的 GPIO1_IO03 这个引脚上,虚拟地址(VA,Virtual Address)、物理地址(PA, Physcical Address)。对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,我们的开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存,经过 MMU 可以将其映射到整个 4GB 的虚拟空间。
Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟 地 址 。 比 如 I.MX6ULL 的 GPIO1_IO03 引 脚 的 复 用寄 存 器IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为0X020E0068。如果没有开启 MMU 的话直接向 0X020E0068 这个寄存器地址写入数据就可以配置 GPIO1_IO03 的复用功能。现在开启了 MMU,并且设置了内存映射,因此就不能直接向 0X020E0068 这个地址写入数据了。我们必须得到 0X020E0068 这个物理地址在 Linux 系统里面对应的虚拟地址,这里就涉及到了物理内存和虚拟内存之间的转换,需要用到两个函数: ioremap 和 iounmap。
I/O 内存访问函数
实验(手动创建设备号、设备节点)
1、建立工程
在共享文件夹新建名为“2_led”文件夹,然后在 2_led 文件夹里面创建 VSCode 工程,工作区命名为“led”。
在 VSCode 上点击文件->打开文件夹…,选刚刚创建的“2_led”文件夹,打开可以看出此时的文件夹“2_led”是空的,点击文件->将工作区另存为…,打开工作区命名对话框,输入要保存的工作区路径和工作区名字
工作区保存成功以后,点击“新建文件”按钮创建led.c和ledApp.c,按下“Ctrl+Shift+P”打开搜索框,然后输入“Edit configurations”,选择“C/C++:Edit configurations…”,打开 C/C++编辑配置文件,会自动在.vscode 目录下生成一个名为 c_cpp_properties.json 的文件,修改成
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/bruce/share/linux/IMX6ULL/linux_kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include",
"/home/bruce/share/linux/IMX6ULL/linux_kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include",
"/home/bruce/share/linux/IMX6ULL/linux_kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
2、编写驱动程序led.c
- 注册模块加载和卸载函数以及添加 LICENSE 和作者信息
- 驱动入口、出口函数
- 在驱动入口、出口函数中分别加入字符设备注册与注销
- 实现设备的具体操作函数open、read、write、release函数
- 在入口函数补充初始化LED的程序:寄存器地址映射、使能GPIO1时钟、设置 GPIO1_IO03 的复用功能GPIO1_IO03,设置 IO 属性、设置 GPIO1_IO03 为输出功能、默认关闭 LED、驱动设备程序
- 补充驱动涉及到的头文件、宏定义、映射后的寄存器虚拟地址、 设备操作函数
- 根据设备操作函数结构体完善具体操作函数open、read、write、release
- 根据向设备写数据write函数补充LED 打开/关闭函数
#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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#define LED_MAJOR 201 /*主设备号*/
#define LED_NAME "led" /*设备名字*/
#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;
/*LED打开/关闭
LEDON(1) 打开 LED, LEDOFF(0) 关闭 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);
}
}
/*打开设备*/
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*从设备读取
filp : 要打开的设备文件(文件描述符)
buf : 返回给用户空间的数据缓冲区
cnt : 要读取的数据长度
offt : 相对于文件首地址的偏移
*/
static ssize_t led_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt)
{
return 0;
}
/*向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
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);/*打开LED灯*/
}else if (ledstat == LEDOFF)
{
led_switch(LEDOFF);/*关闭LED*/
}
return 0;
}
/*关闭/释放设备*/
static int led_release(struct inode *inode,struct file *filp)
{
return 0;
}
/*设备操作函数*/
static struct file_operations led_fops={
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/*驱动入口函数*/
static int __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
/*初始化LED*/
/* 1、寄存器地址映射*/
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);
/* 2、使能GPIO1时钟*/
val =readl(IMX6U_CCM_CCGR1);
val &= ~(3<<26); /*将11左移26位,即26 27位置1,再取反,再与val与运算(同1为1)*/
val |= (3<<26); /*或运算,有1为1,设置新值*/
writel(val,IMX6U_CCM_CCGR1);
/* 3、设置GPIO_IO03的复用功能,将其复用为GPIO_IO03,最后设置IO属性*/
writel(5,SW_MUX_GPIO1_IO03);
/*寄存器SW_PAD_GPIO1_IO03设置IO属性*/
writel(0x10B0,SW_PAD_GPIO1_IO03);
/* 4、设置GPIO1_IO03为输出功能*/
val=readl(GPIO1_GDIR);
val &= ~(1<<3);
val |= (1<<3);
writel(val,GPIO1_GDIR);
/*5、默认关闭*/
val =readl(GPIO1_DR);
val |=(1<<3);
writel(val,GPIO1_DR);
/*6、注册字符设备驱动*/
retvalue =register_chrdev(LED_MAJOR,LED_NAME,&led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}
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);
/*注销字符设备驱动*/
unregister_chrdev(LED_MAJOR,LED_NAME);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bruce");
3、测试应用程序ledApp.c
led 驱动加载成功以后手动创建/dev/led 节点,应用 APP 通过操作/dev/led文件来完成对 LED 设备的控制。向/dev/led 文件写 0 表示关闭 LED 灯,写 1 表示打开 LED 灯。
- 主程序,初始化用到的变量
- 打开led驱动
- 执行打开或关闭操作
- 向/dev/led 文件写入数据
- 关闭文件
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/*----------------------------------------------------------------*/
/* 使用方法 : ./ledtest /dev/led 0 关闭 LED
./ledtest /dev/led 1 打开 LED*/
#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];
/*打开led驱动*/
fd=open(filename,O_RDWR);
if(fd<0){
printf("file %s open failed!\r\n",argv[1]);
return -1;
}
databuf[0]=atoi(argv[2]);/*要执行的操作,打开或关闭*/
/*向/dev/led文件写入数据*/
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;
}
4、编译驱动程序和编译测试应用程序
- 编写Makefile文件
KERNELDIR:=/home/bruce/share/linux/IMX6ULL/linux_kernel/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH:=$(shell pwd)
obj-m:=led.o
build:kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
输入命令make -j32
编译出驱动模块文件:led.ko
- 编译ledApp.c
arm-linux-gnueabihf-gcc ledApp.c -o ledApp
编译成功后会生成 ledApp 这个应用程序
- 将上一小节编译出来的 led.ko和 ledApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中,重启开发板,在软件终端进入到目录 lib/modules/4.1.15 中
-
- 加载led.ko驱动模块
-
- 创建/dev/led设备节点
-
- 测试红色 LED 灯是否点亮
-
- 测试红色 LED 灯是否熄灭
- 测试红色 LED 灯是否熄灭
实验(自动申请设备号、自动创建设备节点)
1、分配和释放设备号
- 使用 register_chrdev 函数注册字符设备的时候只需要给定一个主设备号即可,但是这样会带来两个问题:
①、需要我们事先确定好哪些主设备号没有使用。
②、会将一个主设备号下的所有次设备号都使用掉,比如现在设置 LED 这个主设备号为200,那么 0~1048575(2^20-1)这个区间的次设备号就全部都被 LED 一个设备分走了。
解决这两个问题最好的方法就是要使用设备号的时候向 Linux 内核申请,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号。
申请设备号
1 int major; /* 主设备号 */
2 int minor; /* 次设备号 */
3 dev_t devid; /* 设备号 */
4
5 if (major) { /* 定义了主设备号 */
6 devid = MKDEV(major, 0); /* 大部分驱动次设备号都选择 0 */
7 register_chrdev_region(devid, 1, "test");
8 } else { /* 没有定义设备号 */
9 alloc_chrdev_region(&devid, 0, 1, "test"); /* 申请设备号 */
10 major = MAJOR(devid); /* 获取分配号的主设备号 */
11 minor = MINOR(devid); /* 获取分配号的次设备号 */
12 }
第 1~3 行,定义了主/次设备号变量 major 和 minor,以及设备号变量 devid。
第 5 行,判断主设备号 major是否有效,在 Linux 驱动中一般给出主设备号的话就表示这个设备的设备号已经确定了,因为次设备号基本上都选择 0,这算个 Linux驱动开发中约定俗 成的一种规定了。
第 6 行,如果 major 有效的话就使用 MKDEV 来构建设备号,次设备号选择 0。
第 7 行,使用 register_chrdev_region 函数来注册设备号。
第 9~11 行,如果 major无效,那就表示没有给定设备号。此时就要使用 alloc_chrdev_region 函数来申请设备号。设备号申请成功以后使用 MAJOR 和MINOR 来提取出主设备号和次设备号。
注销设备号
unregister_chrdev_region(devid, 1); /* 注销设备号 */
2、字符设备注册和删除
- 字符设备结构
在 Linux 中使用 cdev 结构体表示一个字符设备,cdev 结构体在 include/linux/cdev.h 文件中的定义
1 struct cdev {
2 struct kobject kobj;
3 struct module *owner;
4 const struct file_operations *ops;
5 struct list_head list;
6 dev_t dev;
7 unsigned int count;
8 };
编写字符设备驱动之前需要定义一个 cdev 结构体变量:struct cdev test_cdev;
-
cdev_init 函数
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。 -
cdev_add 函数
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量):int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量
注册字符设备
1 struct cdev testcdev;
2
3 /* 设备操作函数 */
4 static struct file_operations test_fops = {
5 .owner = THIS_MODULE,
6 /* 其他具体的初始项 */
7 };
8
9 testcdev.owner = THIS_MODULE;
10 cdev_init(&testcdev, &test_fops); /* 初始化 cdev 结构体变量 */
11 cdev_add(&testcdev, devid, 1); /* 添加字符设备 */
申请设备号程序加上注册字符设备代码一起实现的就是函数 register_chrdev 的功能。
- cdev_del 函数
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,void cdev_del(struct cdev *p)
参数 p 就是要删除的字符设备。
删除字符设备
cdev_del(&testcdev); /* 删除 cdev */
注销设备号代码unregister_chrdev_region和删除字符设备代码cdev_del 相当于 unregister_chrdev 函数。
3、自动创建设备节点
- 创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。
class_create 是类创建函数, class_create 是个宏定义
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy
void class_destroy(struct class *cls);
参数 cls 就是要删除的类
- 创建和卸载设备
在这个类下创建一个设备。使用 device_create 函数在类下面创建设备
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。返回值就是创建好的设备。
卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy
void device_destroy(struct class *class, dev_t devt)
参数 classs 是要删除的设备所处的类,参数 devt 是要删除的设备号。
在驱动入口函数里面创建类和设备,在驱动出口函数里面删除类和设备
1 struct class *class; /* 类 */
2 struct device *device; /* 设备 */
3 dev_t devid; /* 设备号 */
4
5 /* 驱动入口函数 */
6 static int __init xxx_init(void)
7 {
8 /* 创建类 */
9 class = class_create(THIS_MODULE, "xxx");
10 /* 创建设备 */
11 device = device_create(class, NULL, devid, NULL, "xxx");
12 return 0;
13 }
14
15 /* 驱动出口函数 */
16 static void __exit led_exit(void)
17 {
18 /* 删除设备 */
19 device_destroy(newchrled.class, newchrled.devid);
20 /* 删除类 */
21 class_destroy(newchrled.class);
22 }
23
24 module_init(led_init);
25 module_exit(led_exit);
4、设置文件私有数据
每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等等,在编写驱动的时候你可以将这些属性全部写成变量的形式。
编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中:
/* 设备结构体 */
1 struct test_dev{
2 dev_t devid; /* 设备号 */
3 struct cdev cdev; /* cdev */
4 struct class *class; /* 类 */
5 struct device *device; /* 设备 */
6 int major; /* 主设备号 */
7 int minor; /* 次设备号 */
8 };
9
10 struct test_dev testdev;
11
12 /* open 函数 */
13 static int test_open(struct inode *inode, struct file *filp)
14 {
15 filp->private_data = &testdev; /* 设置私有数据 */
16 return 0;
17 }
在 open 函数里面设置好私有数据以后,在 write、 read、 close 等函数中直接读取 private_data即可得到设备结构体。
5、程序编写
在2_led基础上进行修改,复制2_led文件夹改名3_newchrled,
将工程中的文件名修改,并赋予权限sudo chmod 777 xxx
驱动程序编写
将led.c中的注册和注销字符设备驱动的代码修改
驱动入口函数
- 创建设备号
- 初始化cdev字符设备
- 添加一个字符设备cdev
- 创建类
- 创建设备
驱动出口函数
- 删除字符设备和设备号
- 删除设备和类
创建设备结构并定义
struct newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev;
struct class *class;/* 类 */
struct device *device;/*设备*/
int major;
int minor;
};
struct newchrled_dev newchrled;
在 open 的时候将 private_data 指向设备结构体
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
return 0;
}
newchrled.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>
#define NEWCHRLED_CNT 1 /*设备号个数*/
#define NEWCHRLED_NAME "newchrled" /*设备名字*/
#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 newchrled_dev{
dev_t devid; /* 设备号 */
struct cdev cdev;
struct class *class;/* 类 */
struct device *device;/*设备*/
int major;
int minor;
};
/*led设备*/
struct newchrled_dev newchrled;
/*LED打开/关闭
LEDON(1) 打开 LED, LEDOFF(0) 关闭 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);
}
}
/*打开设备*/
static int led_open(struct inode *inode, struct file *filp)
{
filp->private_data = &newchrled; /* 设置私有数据 */
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);/*打开LED灯*/
}else if (ledstat == LEDOFF)
{
led_switch(LEDOFF);/*关闭LED*/
}
return 0;
}
/*关闭/释放设备*/
static int led_release(struct inode *inode,struct file *filp)
{
return 0;
}
/*设备操作函数*/
static struct file_operations newchrled_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;
/*初始化LED*/
/* 1、寄存器地址映射*/
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);
/* 2、使能GPIO1时钟*/
val =readl(IMX6U_CCM_CCGR1);
val &= ~(3<<26); /*将11左移26位,即26 27位置1,再取反,再与val与运算(同1为1)*/
val |= (3<<26); /*或运算,有1为1,设置新值*/
writel(val,IMX6U_CCM_CCGR1);
/* 3、设置GPIO_IO03的复用功能,将其复用为GPIO_IO03,最后设置IO属性*/
writel(5,SW_MUX_GPIO1_IO03);
/*寄存器SW_PAD_GPIO1_IO03设置IO属性*/
writel(0x10B0,SW_PAD_GPIO1_IO03);
/* 4、设置GPIO1_IO03为输出功能*/
val=readl(GPIO1_GDIR);
val &= ~(1<<3);
val |= (1<<3);
writel(val,GPIO1_GDIR);
/*5、默认关闭*/
val =readl(GPIO1_DR);
val |=(1<<3);
writel(val,GPIO1_DR);
/*注册字符设备驱动*/
/*01\创建设备号*/
/* retvalue =register_chrdev(LED_MAJOR,LED_NAME,&led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}*/
if(newchrled.major){ /* 定义了设备号 */
newchrled.devid = MKDEV(newchrled.major, 0);
register_chrdev_region(newchrled.devid,NEWCHRLED_CNT,NEWCHRLED_NAME);
}else{ /*没有定义设备号 */
alloc_chrdev_region(&newchrled.devid,0,NEWCHRLED_CNT,NEWCHRLED_NAME);
newchrled.major=MAJOR(newchrled.devid);
newchrled.minor=MINOR(newchrled.devid);
}
printk("newchrled major=%d,minor=%d\r\n",newchrled.major,newchrled.minor);
/*02\初始化cdev*/
newchrled.cdev.owner=THIS_MODULE;
cdev_init(&newchrled.cdev,&newchrled_fops);
/*03\添加一个cdev*/
cdev_add(&newchrled.cdev,newchrled.devid,NEWCHRLED_CNT);
/*04\创建类*/
newchrled.class=class_create(THIS_MODULE,NEWCHRLED_NAME);
if(IS_ERR(newchrled.class)){
return PTR_ERR(newchrled.class);
}
/*05\创建设备*/
newchrled.device=device_create(newchrled.class,NULL,newchrled.devid,NULL,NEWCHRLED_NAME);
if(IS_ERR(newchrled.device)){
return PTR_ERR(newchrled.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);
/*注销字符设备驱动*/
//unregister_chrdev(LED_MAJOR,LED_NAME);
/*删除字符设备和设备号*/
cdev_del(&newchrled.cdev);
unregister_chrdev_region(newchrled.devid,NEWCHRLED_CNT);
/*删除设备和类*/
device_destroy(newchrled.class,newchrled.devid);
class_destroy(newchrled.class);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("bruce");
newchrledApp.c与ledApp.c代码一样,Makefile中设置 obj-m 变量的值为 newchrled.o
运行测试
-
在linux终端输入命令
make -j32
编译出驱动模块文件:newchrled.ko -
编译lednewchrledApp.c
arm-linux-gnueabihf-gcc lednewchrledApp.c -o lednewchrledApp
编译成功后会生成newchrledApp 这个应用程序
- 将上一小节编译出来的 newchrled.ko和 newchrledApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中,重启开发板,在软件终端进入到目录 lib/modules/4.1.15 中
-
- 加载led.ko驱动模块
insmod newchrled.ko
驱动加载成功以后会输出申请到的主设备号和次设备号(自动申请,不需要手动)
- 加载led.ko驱动模块
-
- 驱动加载成功以后会自动在/dev 目录下创建设备节点文件/dev/newchrled,输入如下命令查看/dev/newchrled 这个设备节点文件是否存在:
ls /dev/newchrled -l
- 驱动加载成功以后会自动在/dev 目录下创建设备节点文件/dev/newchrled,输入如下命令查看/dev/newchrled 这个设备节点文件是否存在:
-
- 测试红色 LED 灯是否点亮
./newchrledApp /dev/newchrled 1
- 测试红色 LED 灯是否点亮
-
- 测试红色 LED 灯是否熄灭
./newchrledApp /dev/newchrled 0
- 测试红色 LED 灯是否熄灭