之前学习了如何在字符驱动设备模块中打印Hello World,这次顺着路径我们来学习用字符设备驱动点亮LED灯。
配置必要工具
我们这次会用到一个叫gpio readall的命令,效果如下
它可以展示我们树莓派的各个引脚状态。
下载方式
git clone https://github.com/WiringPi/WiringPi.git
cd ~/wiringPi
./build
gpio -v //如果显示版本号则说明下载成功
选择引脚
看上图,BCM代表的是具体的Pin口,我们选择GPIO4
接着我们输入 cat /proc/iomem 查看GPIO的地址映射,得到为0xfe200000
从芯片手册上可以看到,我们一共要设置三个寄存器:
GPFSEL寄存器
我们要控制GPIO4,所以是FSEL4
从芯片手册给我们的示例来看,000设置为输入,001设置为输出
也就是说,如果我们想要点亮LED灯的话,要将12~14为设置为001
而GPIO的地址映射为0xfe200000
所以应该是0xfe200000 |= 001 << (3 * 4);//3*4==12,是为了方便改成其他引脚才这样写
GPSET寄存器
顾名思义,这个寄存器是用来设置高电平的,并且GPSET0可配置GPIO(0-31),GPSET1可配置GPIO(32-57),给这个寄存器置1则输出高电平,置0无效,所以我们为了点亮LED灯,选择置1,同理,我们看芯片手册发现其地址偏移为0x1c,也就是0xfe20001c,
也就是 0xfe20001c |= 0x01 << 4;//GPIO4是第4位
GPCLR寄存器
这个寄存器是用来清除标志位的,和GPSET寄存器是一样的用法,而它的地址偏移位是0x28,
所以 0xfe200028 |= 0x01 << 4;//GPIO4是第4位
我们已经成功配置好寄存器了,接下来就是配置设备了
配置字符设备
首先我们要定义一些设备信息,以用于向内核注册设备
static int major_num, minor_num; // 定义主设备号和次设备号
struct cdev cdev; // 定义字符设备结构体
static dev_t dev_num; // 定义设备号
struct class *class; // 定义设备类指针
struct device *device; // 定义设备指针
接着需要向内核注册设备号,有静态和动态两种方式,这里推荐动态,因为更安全,静态是自己定义好设备号,动态方法是由内核分配,不会引起冲突
if (major_num) // 如果提供了主设备号,进行静态注册
{
printk("mjor_num=%d \n", major_num);
printk("minor_num=%d \n", minor_num);
// 将cdev添加到核心的设备号
dev_num = MKDEV(major_num, minor_num);
ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);
if (ret < 0)
{
printk("register_chrdev_region error\n");
}
printk("register_chrdev_region success\n");
}
else // 动态注册设备号
{
ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR, DEVICE_NUMBER, DEVICE_ANANME);
if (ret < 0)
{
printk("alloc_chrdev_region error\n");
}
printk("alloc_chrdev_region success\n");
major_num = MAJOR(dev_num); // 获取主设备号
minor_num = MINOR(dev_num); // 获取次设备号
printk("mjor_num=%d \n", major_num);
printk("minor_num=%d \n", minor_num);
}
定义好设备号后,就需要将设备注册到内核中并在内核中创建对应的节点,以便于访问
// 初始化cdev
cdev.owner = THIS_MODULE;
// 将file_operations结构体绑定到cdev
cdev_init(&cdev, &chrdev_ops);
// 向系统注册设备,使用户能够访问设备
cdev_add(&cdev, dev_num, DEVICE_NUMBER);
// 创建设备类
class = class_create(THIS_MODULE, DEVICE_CLASS_NAME);
// 创建设备节点
device = device_create(class, NULL, dev_num, NULL, DEVICE_NODE_NAME);
现在我们可以控制设备了
现在我们对GPIO4的物理地址进行虚拟映射,方便我们进行寄存器的配置
// 对物理地址进行虚拟地址映射,从而进行操作
vir_gpio4_dr = ioremap(0xfe200000, 4);
if (vir_gpio4_dr == NULL)
{
printk("gpio4dr ioremap error\n");
return -EBUSY;
}
vir_gpio4_h = ioremap(GPIO4_H, 4);
if (vir_gpio4_h == NULL)
{
printk("gpio4h ioremap error\n");
return -EBUSY;
}
vir_gpio4_l = ioremap(GPIO4_L, 4);
if (vir_gpio4_l == NULL)
{
printk("gpio4l ioremap error\n");
return -EBUSY;
}
printk("gpio ioremap success\n");
后面的思路就是,我们输入1,则将GPSET寄存器置1,输入0则将GPCLR寄存器置0,这样就可以实现控制LED的亮灭了
下面是全部的代码
代码
led.c
#include <linux/init.h> // 初始化头文件
#include <linux/module.h> // 最基本的文件,支持动态添加和卸载模块
#include <linux/moduleparam.h> // 驱动传参头文件
#include <linux/fs.h> // 文件操作相关的struct定义
#include <linux/kdev_t.h> // 设备号相关的宏定义
#include <linux/cdev.h> // 字符设备相关的头文件
#include <linux/device.h> // 设备模型相关的头文件
#include <linux/io.h> // 内存映射IO操作相关头文件
#define DEVICE_NUMBER 1 // 次设备号个数
#define DEVICE_SNAME "schrdev" // 静态注册设备的名字
#define DEVICE_ANANME "achrdev" // 动态注册设备的名字
#define DEVICE_MINOR 0 // 次设备号起始地址
#define DEVICE_CLASS_NAME "chrdev_class" // 设备类名称
#define DEVICE_NODE_NAME "chrdev_test" // 设备节点名称
// GPIO寄存器的物理地址定义
#define GPIO4_DR 0xfe200000
#define GPIO4_H 0xfe20001c
#define GPIO4_L 0xfe200028
// 定义虚拟地址指针
unsigned int *vir_gpio4_dr = NULL;
unsigned int *vir_gpio4_h = NULL;
unsigned int *vir_gpio4_l = NULL;
static int major_num, minor_num; // 定义主设备号和次设备号
struct cdev cdev; // 定义字符设备结构体
static dev_t dev_num; // 定义设备号
struct class *class; // 定义设备类指针
struct device *device; // 定义设备指针
// 打开设备时的操作
int chrdev_open(struct inode *inode, struct file *file)
{
printk("hello chrdev_open \n"); // 打印打开信息
return 0;
}
// 写操作
ssize_t chrdev_write(struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t)
{
char kbuf[64] = {0};
// 将用户空间的数据复制到内核空间
if (copy_from_user(kbuf, ubuf, size) != 0)
{
printk("copy_from_user error\n");
return -1;
}
printk("kbuf is %s\n", kbuf);
// 设置GPIO寄存器
*vir_gpio4_dr |= (001 << (3 * 4));
if (kbuf[0] == 1)
{
// 设置GPIO高电平
*vir_gpio4_h |= (1 << 4);
}
else if (kbuf[0] == 0)
{
// 设置GPIO低电平
*vir_gpio4_l |= (1 << 4);
}
return 0;
}
// 定义文件操作结构体
struct file_operations chrdev_ops =
{
.owner = THIS_MODULE,
.open = chrdev_open,
.write = chrdev_write,
};
// 定义模块参数
module_param(major_num, int, S_IRUGO);
module_param(minor_num, int, S_IRUGO);
// 模块参数描述
MODULE_PARM_DESC(major_num, "e.g:a=1");
MODULE_PARM_DESC(minor_num, "e.g:a=1");
// 模块初始化函数
static int chrdev_init(void)
{
int ret;
if (major_num) // 如果提供了主设备号,进行静态注册
{
printk("mjor_num=%d \n", major_num);
printk("minor_num=%d \n", minor_num);
// 将cdev添加到核心的设备号
dev_num = MKDEV(major_num, minor_num);
ret = register_chrdev_region(dev_num, DEVICE_NUMBER, DEVICE_SNAME);
if (ret < 0)
{
printk("register_chrdev_region error\n");
}
printk("register_chrdev_region success\n");
}
else // 动态注册设备号
{
ret = alloc_chrdev_region(&dev_num, DEVICE_MINOR, DEVICE_NUMBER, DEVICE_ANANME);
if (ret < 0)
{
printk("alloc_chrdev_region error\n");
}
printk("alloc_chrdev_region success\n");
major_num = MAJOR(dev_num); // 获取主设备号
minor_num = MINOR(dev_num); // 获取次设备号
printk("mjor_num=%d \n", major_num);
printk("minor_num=%d \n", minor_num);
}
// 初始化cdev
cdev.owner = THIS_MODULE;
// 将file_operations结构体绑定到cdev
cdev_init(&cdev, &chrdev_ops);
// 向系统注册设备,使用户能够访问设备
cdev_add(&cdev, dev_num, DEVICE_NUMBER);
// 创建设备类
class = class_create(THIS_MODULE, DEVICE_CLASS_NAME);
// 创建设备节点
device = device_create(class, NULL, dev_num, NULL, DEVICE_NODE_NAME);
// 对物理地址进行虚拟地址映射,从而进行操作
vir_gpio4_dr = ioremap(0xfe200000, 4);
if (vir_gpio4_dr == NULL)
{
printk("gpio4dr ioremap error\n");
return -EBUSY;
}
vir_gpio4_h = ioremap(GPIO4_H, 4);
if (vir_gpio4_h == NULL)
{
printk("gpio4h ioremap error\n");
return -EBUSY;
}
vir_gpio4_l = ioremap(GPIO4_L, 4);
if (vir_gpio4_l == NULL)
{
printk("gpio4l ioremap error\n");
return -EBUSY;
}
printk("gpio ioremap success\n");
return 0;
}
// 模块卸载函数
static void chrdev_exit(void)
{
unregister_chrdev_region(MKDEV(major_num, minor_num), DEVICE_NUMBER); // 释放设备号
cdev_del(&cdev); // 删除cdev
device_destroy(class, dev_num); // 销毁设备节点
class_destroy(class); // 销毁设备类
printk("bye bye \n");
}
// 指定模块初始化和卸载函数
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL"); // 指定模块的许可证为GPL
app.c
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc,char *argv[])
{
int fd;
char buf[64] = "0";
fd = open( "/dev/chrdev_test",O_RDWR);//打开设备节点
if(fd < 0)
{
perror( "open error \n");
return fd;
}
buf[0]= atoi( argv[1]);
write( fd,buf,sizeof(buf)); //向内核层写数据
close( fd);
return 0;
}
Makefile
# 判断是否在内核构建系统内。如果没有定义 KERNELRELEASE,则表示这是从命令行调用。
ifneq ($(KERNELRELEASE),)
# 如果不是内核构建系统,则定义需要编译的模块对象文件。
# obj-m 是内核模块的编译变量,+= 表示添加模块文件(.o)
obj-m += led.o
else
# 定义内核头文件的位置,使用当前正在运行的内核版本。
KDIR := /home/interest/linux/lib/modules/$(shell uname -r)/build
# 定义当前的工作目录。
PWD := $(shell pwd)
# 默认目标。如果调用了 make 而没有指定目标,会执行这个部分。
# -C $(KDIR) 表示切换到内核源码目录进行编译
# M=$(PWD) 表示在当前模块的目录下执行内核模块编译
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
# 清理目标:用于清理编译产生的中间文件。
clean:
rm -f *.mod.c *.order *.ko *.o *.mod *.symvers
endif
编译
Make
gcc app.c -o app
结果如图则成功
运行
sudo insmod led.ko
sudo ./app 1
gpio readall
结果可以看到
说明GPIO4为输出模式,并且为1
sudo ./app 0
gpio readall
sudo rmmod led
此时GPIO4已经变化
总结
字符设备驱动点亮LED总体上来说,只要找到对应的寄存器并配置好,然后通过控制寄存器就可以控制LED的亮灭了,其他的代码可以说是框架代码,谢谢观看。