树莓派驱动开发(三)字符设备驱动之点亮LED

之前学习了如何在字符驱动设备模块中打印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的亮灭了,其他的代码可以说是框架代码,谢谢观看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值