【Linux 字符设备驱动:控制 LED 灯】

63 阅读11分钟

前言 在 Linux 驱动开发领域,字符设备驱动是最基础且常用的类型之一。本文将通过一个控制 LED 的实例,详细讲解如何编写、加载和使用 Linux 字符设备驱动,帮助你快速入门驱动开发。无论是嵌入式开发板还是普通 Linux 系统,掌握字符驱动的编写方法都至关重要。

一、什么是字符设备驱动?

在 Linux 系统中,设备被抽象为文件,通过文件操作接口(open、read、write 等)进行访问。字符设备是一种按字节流顺序访问的设备,例如串口、键盘、LED 控制器等。字符设备驱动则负责实现这些文件操作接口,将用户空间的请求转化为对硬件的控制。

二、LED硬件配置

开发板原理图,可以看到使用的是GPIO0_B7 在这里插入图片描述

三、编写字符设备驱动代码

1.修改设备树

由于led9这个设备默认被系统使用了,如果要自己使用的话需要在设备树中先将led9屏蔽,可以通过GPIO0_B7快速定位到设备节点。修改之前可以对原来的文件做一个备份。

cp rk3568-evb.dtsi rk3568-evb.dtsi_bak

在这里插入图片描述

/home/chenmy/rk356x/RK356X_Android11.0/kernel/arch/arm64/boot/dts/rockchip/rk3568-evb.dtsi

修改完成后,需要从新编译kernel。

source build/envsetup.sh

lunch 55

./build.sh -AC

再对开发板进行boot.img烧录。

2.编写驱动代码

准备工作:RK3568开发板GPIO引脚计算。 在这里插入图片描述 通过芯片手册已知开发板有 5 组 GPIO bank:GPIO0GPIO4,每组又以 A0A7, B0B7, C0C7, D0~D7 作为编号区分,常用以下公式计算引脚:

GPIO pin脚计算公式:pin = bank * 32 + number

GPIO 小组编号计算公式:number = group * 8 + X

下面演示GPIO0_B7 pin脚计算方法:

bank = 0; //GPIO0_B7

group = 1; //GPIO0_B7, (A=0), (B=1), (C=2), (D=3)

X = 7; //GPIO0_B7, [0,7]

number = group * 8 + X = 1 * 8 + 7 = 15

pin = bank * 32 + number = 0 * 32 + 15 = 15;

所以GPIO0_B7这个pin脚最后计算为15。

3.通过GPIO控制LED

在 Linux 内核开发中,GPIO(通用输入输出)驱动是最基础的驱动类型之一。内核提供了一系列标准接口用于操作 GPIO,这些接口抽象了不同硬件平台的差异,提高了驱动的可移植性。以下是 本例中GPIO 驱动中常用的接口及其详细说明:

#include <linux/gpio.h>

// 请求GPIO(返回0成功,负数错误码)
int gpio_request(unsigned gpio, const char *label);

// 释放GPIO
void gpio_free(unsigned gpio);

// 读取输入值(返回0或1)
int gpiod_get_value(const struct gpio_desc *desc);

// 设置输出值(val为0或1)
void gpiod_set_value(struct gpio_desc *desc, int val);

/**
 * gpio_direction_output - 将 GPIO 设置为输出模式并初始化值
 * @gpio:     GPIO 编号(如 17 代表 GPIO17)
 * @value:    初始输出值(0 = 低电平,1 = 高电平)
 * 返回值:    0 表示成功,负数表示错误
 */
int gpio_direction_output(unsigned gpio, int value);

led9.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/gpio.h>
#include <linux/delay.h>

// 设备信息
#define DEVICE_NAME "led9"
#define LED9 15 //pin脚
// 设备结构体
typedef struct {
    struct cdev cdev;
    dev_t dev_num;
    struct class *class;
    struct device *device;
} s_led9;

static s_led9 led_dev;




static int led9_open(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "LED9 led9_open\n");
    return 0;
}

static int led9_release(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "LED9 led9_release\n");
    return 0;
}


static ssize_t led9_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    int value = 0;
    if (copy_from_user(&value, buf, sizeof(int))) {
        return -EFAULT;
    }
    
    printk(KERN_INFO "LED9 led9_write value = %d\n", value);

    gpio_set_value(LED9, value); 
    
    return 0;
}

// 读取LED状态
static ssize_t led9_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    int value = gpio_get_value(LED9);
    
    if (copy_to_user(buf, &value, sizeof(int))) {
        return -EFAULT;
    }
    printk(KERN_INFO "LED9 led9_read value = %d\n", value);
    
    return 0;
}




// 文件操作结构体
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led9_open,
    .release = led9_release,
    .write = led9_write,
    .read = led9_read,
};

// 模块初始化函数
static int __init led9_init(void) {
    int result;
    
    // 动态分配设备号
    result = alloc_chrdev_region(&led_dev.dev_num, 0, 1, DEVICE_NAME);
    if (result < 0) {
        printk(KERN_ERR "Failed to allocate major number\n");
        return result;
    }
    
    printk(KERN_INFO "Allocated major number: %d\n", MAJOR(led_dev.dev_num));
    
    // 初始化cdev结构
    cdev_init(&led_dev.cdev, &led_fops);
    led_dev.cdev.owner = THIS_MODULE;
    
    // 添加字符设备
    result = cdev_add(&led_dev.cdev, led_dev.dev_num, 1);
    if (result < 0) {
        printk(KERN_ERR "Failed to add cdev\n");
        unregister_chrdev_region(led_dev.dev_num, 1);
        return result;
    }
    
    // 创建类
    led_dev.class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(led_dev.class)) {
        printk(KERN_ERR "Failed to create class\n");
        cdev_del(&led_dev.cdev);
        unregister_chrdev_region(led_dev.dev_num, 1);
        return PTR_ERR(led_dev.class);
    }
    
    // 创建设备节点
    led_dev.device = device_create(led_dev.class, NULL, led_dev.dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(led_dev.device)) {
        printk(KERN_ERR "Failed to create device\n");
        class_destroy(led_dev.class);
        cdev_del(&led_dev.cdev);
        unregister_chrdev_region(led_dev.dev_num, 1);
        return PTR_ERR(led_dev.device);
    }
    
    // 配置GPIO
    if (gpio_request(LED9, "led9")) {
        printk(KERN_ERR "Failed to request GPIO\n");
        device_destroy(led_dev.class, led_dev.dev_num);
        class_destroy(led_dev.class);
        cdev_del(&led_dev.cdev);
        unregister_chrdev_region(led_dev.dev_num, 1);
        return -EBUSY;
    }
    
    // 设置GPIO为输出模式
    gpio_direction_output(LED9, 0);
    
    
    printk(KERN_INFO "LED driver initialized successfully\n");
    return 0;
}

// 模块退出函数
static void __exit led9_exit(void) {
    // 关闭LED
    gpio_set_value(LED9, 0);
    
    // 释放GPIO
    gpio_free(LED9);
    
    // 移除设备节点和类
    device_destroy(led_dev.class, led_dev.dev_num);
    class_destroy(led_dev.class);
    
    // 移除字符设备
    cdev_del(&led_dev.cdev);
    
    // 释放设备号
    unregister_chrdev_region(led_dev.dev_num, 1);
    
    printk(KERN_INFO "LED driver exited successfully\n");
}

module_init(led9_init);
module_exit(led9_exit); 


MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("LED Control Character Device Driver");
MODULE_LICENSE("GPL");

Makefile

export ARCH=arm64

export CROSS_COMPILE=/home/chenmy/rk356x/RK356X_Android11.0/prebuilts/gcc/linux-x86/aarch64/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin/aarch64-linux-gnu-

obj-m += led9.o

KERNEL_DIR:=/home/chenmy/rk356x/RK356X_Android11.0/kernel

all:
	make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
	make -C $(KERNEL_DIR) M=$(PWD) clean

4.通过寄存器控制LED

1.GPIO 寄存器基础

GPIO 控制器是连接 CPU 和外部设备的桥梁,其核心是一组可读写的寄存器,主要包括:

方向寄存器(Direction Register) 控制引脚是作为输入还是输出。 典型位定义:1表示输出,0表示输入。

数据寄存器(Data Register) 输出模式:写入数据以控制引脚电平。 输入模式:读取数据以获取引脚当前电平。

上拉 / 下拉寄存器(Pull-up/down Register) 配置引脚内部上拉或下拉电阻,增强抗干扰能力。

2.寄存器操作步骤

通过寄存器控制 LED 通常需要以下步骤:

确定 GPIO 基地址 查阅芯片手册获取 GPIO 控制器的物理基地址。 例如,RK3568的 GPIO0 基地址为0xFDD60000。 配置引脚为输出模式 通过写入方向寄存器将目标引脚设置为输出。 控制引脚电平 通过写入数据寄存器控制引脚输出高 / 低电平。

GPIO0 基地址为0xFDD60000 在这里插入图片描述 方向寄存器偏移地址:基地址 + 0x0008 数据寄存器偏移地址:基地址 + 0x0000 在这里插入图片描述 在这里插入图片描述 可以看到手册中方向寄存器A0-A7,B0-B7对应GPIO_SWPORT_DDR_L,0-15bit对应每个引脚的输入输出模式,16-31bit对应输入输出模式的使能。数据寄存器对应的GPIO_SWPORT_DR_L, 0-15bit对应每个引脚的高低电平,16-31bit对应数据的使能。

在 Linux 内核开发中,操作硬件设备通常需要通过内存映射和寄存器访问。以下是四个核心函数的详细介绍:

/*
功能
将硬件设备的物理地址空间映射到内核的虚拟地址空间,以便内核可以像访问内存一样访问硬件寄存器。
参数
phys_addr:硬件设备的物理基地址(如 GPIO 控制器的基址)。
size:需要映射的地址范围大小(单位:字节)。
返回值
成功:返回映射后的虚拟地址指针(类型为 void __iomem *)。
失败:返回 NULL。
*/
void __iomem *ioremap(resource_size_t phys_addr, size_t size);
/*
功能
释放由 ioremap() 创建的虚拟地址映射,结束对硬件寄存器的访问。
参数
addr:ioremap() 返回的虚拟地址指针。
*/
void iounmap(void __iomem *addr);
/*
功能
向指定的硬件寄存器写入 64 位数据,确保正确处理内存屏障和字节序。
参数
value:要写入的 64 位数据(类型为 u64,即 unsigned long long)。
addr:映射后的虚拟地址(通常由 ioremap() 返回)。
*/
void iowrite64(u64 value, void __iomem *addr);
/*
功能
从指定的硬件寄存器读取 64 位数据,确保正确处理内存屏障和字节序。
参数
addr:映射后的虚拟地址。
返回值
读取的 64 位数据(类型为 u64)。
*/
u64 ioread64(const void __iomem *addr);

led_ioremap.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/device.h>

// 设备信息
#define DEVICE_NAME "led9_ioremap"
#define GPIO_BASE_PHYS (0xFDD60000) 
#define LED9_PIN 15
#define GPIO_SIZE 0x10000           // 映射大小(64k)

#define GPIO_SWPORT_DDR 0x0008 //输入输出偏移量
#define GPIO_SWPORT_DR 0x0000 //高低电平偏移量

// 设备结构体
typedef struct {
    struct cdev cdev;
    dev_t dev_num;
    struct class *class;
    struct device *device;
    void __iomem *gpio_base;
} s_led9;

static s_led9 led_dev;




static int led9_ioremap_open(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "LED9 led9_ioremap_open\n");
    return 0;
}

static int led9_ioremap_release(struct inode *inode, struct file *filp) {
    printk(KERN_INFO "LED9 led9_ioremap_release\n");
    return 0;
}


static ssize_t led9_ioremap_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) {
    int value = 0;
    if (copy_from_user(&value, buf, sizeof(int))) {
        return -EFAULT;
    }
    if (!led_dev.gpio_base) 
    {
        printk(KERN_ERR "led9_ioremap_write gpio_base can't be null\n");
        return -EFAULT;
    }
    
    u64 gp_cfg = ioread64(led_dev.gpio_base + GPIO_SWPORT_DR);
    printk(KERN_INFO "led9_ioremap_write GPIO_SWPORT_DR gp_cfg = %llx\n", gp_cfg);
    if(value)
    {
        gp_cfg |= 1 << 31;//Write access enable
        gp_cfg |= 1 << 15;//high

    }
    else
    {
        gp_cfg |= 1 << 31;//Write access enable
        printk(KERN_INFO "LED9 led9_ioremap_write Write access gp_cfg = %llx\n", gp_cfg);
        gp_cfg &= ~(1 << 15);//low
        printk(KERN_INFO "LED9 led9_ioremap_write low gp_cfg = %llx\n", gp_cfg);
    }

    
    iowrite64(gp_cfg, led_dev.gpio_base + GPIO_SWPORT_DR);
    
    printk(KERN_INFO "LED9 led9_ioremap_write gp_cfg = %llx\n", gp_cfg);

    return 0;
}

// 读取LED状态
static ssize_t led9_ioremap_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
    int value = 0;
    
    if (!led_dev.gpio_base) 
    {
        printk(KERN_ERR "led9_ioremap_read gpio_base can't be null\n");
        return -EFAULT;
    }
        
    u64 gp_cfg = ioread64(led_dev.gpio_base + GPIO_SWPORT_DR);
    value = (gp_cfg >> 15) & 1;//获取第15位

    printk(KERN_INFO "LED9 led9_ioremap_read value = %d\n", value);
    if (copy_to_user(buf, &value, sizeof(int))) {
        return -EFAULT;
    }
    
    return 0;
}




// 文件操作结构体
static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led9_ioremap_open,
    .release = led9_ioremap_release,
    .write = led9_ioremap_write,
    .read = led9_ioremap_read,
};

// 模块初始化函数
static int __init led9_ioremap_init(void) {
    int result;
    
    // 动态分配设备号
    result = alloc_chrdev_region(&led_dev.dev_num, 0, 1, DEVICE_NAME);
    if (result < 0) {
        printk(KERN_ERR "led9_ioremap_init Failed to allocate major number\n");
        return result;
    }
    
    printk(KERN_INFO "led9_ioremap_init Allocated major number: %d\n", MAJOR(led_dev.dev_num));
    
    // 初始化cdev结构
    cdev_init(&led_dev.cdev, &led_fops);
    led_dev.cdev.owner = THIS_MODULE;
    
    // 添加字符设备
    result = cdev_add(&led_dev.cdev, led_dev.dev_num, 1);
    if (result < 0) {
        printk(KERN_ERR "led9_ioremap_init Failed to add cdev\n");
        unregister_chrdev_region(led_dev.dev_num, 1);
        return result;
    }
    
    // 创建类
    led_dev.class = class_create(THIS_MODULE, DEVICE_NAME);
    if (IS_ERR(led_dev.class)) {
        printk(KERN_ERR "led9_ioremap_init Failed to create class\n");
        cdev_del(&led_dev.cdev);
        unregister_chrdev_region(led_dev.dev_num, 1);
        return PTR_ERR(led_dev.class);
    }
    
    // 创建设备节点
    led_dev.device = device_create(led_dev.class, NULL, led_dev.dev_num, NULL, DEVICE_NAME);
    if (IS_ERR(led_dev.device)) {
        printk(KERN_ERR "led9_ioremap_init Failed to create device\n");
        class_destroy(led_dev.class);
        cdev_del(&led_dev.cdev);
        unregister_chrdev_region(led_dev.dev_num, 1);
        return PTR_ERR(led_dev.device);
    }
    
    // 映射GPIO寄存器
    led_dev.gpio_base = ioremap(GPIO_BASE_PHYS, GPIO_SIZE);
    if (!led_dev.gpio_base) {
        printk(KERN_ERR "led9_ioremap_init Failed to ioremap\n");
        class_destroy(led_dev.class);
        cdev_del(&led_dev.cdev);
        unregister_chrdev_region(led_dev.dev_num, 1);
        return -ENOMEM;
    }
    printk(KERN_INFO "led9_ioremap_init led_dev.gpio_base = %llx\n", led_dev.gpio_base);


    u64 gp_cfg = ioread64(led_dev.gpio_base + GPIO_SWPORT_DDR);
    printk(KERN_INFO "led9_ioremap_init GPIO_SWPORT_DDR gp_cfg = %llx\n", gp_cfg);
    gp_cfg |= 1 << 31;// Write access enable
    gp_cfg |= 1 << 15;//Output

    //设置输出模式
    iowrite64(gp_cfg, led_dev.gpio_base + GPIO_SWPORT_DDR);
    gp_cfg = ioread64(led_dev.gpio_base + GPIO_SWPORT_DDR);


    printk(KERN_INFO "led9_ioremap_init LED driver initialized successfully gp_cfg = %llx\n", gp_cfg);
    return 0;
}

// 模块退出函数
static void __exit led9_ioremap_exit(void) {

    if (led_dev.gpio_base) {
        iounmap(led_dev.gpio_base);  // 解除映射
        led_dev.gpio_base = NULL;
    }

    
    // 移除设备节点和类
    device_destroy(led_dev.class, led_dev.dev_num);
    class_destroy(led_dev.class);
    
    // 移除字符设备
    cdev_del(&led_dev.cdev);
    
    // 释放设备号
    unregister_chrdev_region(led_dev.dev_num, 1);
    
    printk(KERN_INFO "led9_ioremap_exit LED driver exited successfully\n");
}

module_init(led9_ioremap_init);
module_exit(led9_ioremap_exit); 


MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("LED _ioremap Control Character Device Driver");
MODULE_LICENSE("GPL");

四、调试与验证

这里我写了一个Android测试程序。 native-lib.cpp

#include <jni.h>
#include <string>
#include <fcntl.h>
#include <unistd.h>
#include <android/log.h>

#define TAG "LED9"

#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)


extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_open(JNIEnv *env, jobject thiz) {

    int fd;
    fd = open("/dev/led9", O_RDWR);
    LOGD("Device opened (fd=%d)\n", fd);
    return fd;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_read(JNIEnv *env, jobject thiz, jint fd) {
    int value = 0;
    if (read(fd, &value, sizeof(int)) != 0) {
        LOGD("Failed to read");
        return -1;
    }
    LOGD("Data read from device: %d\n", value);
    return value;

}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_write(JNIEnv *env, jobject thiz, jint fd, jint value) {

    if (write(fd, &value, sizeof(int)) != 0) {
        LOGD("Failed to write to device");
        return -1;
    }
    LOGD("Data written to device value = %d\n", value);
    return 0;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_close(JNIEnv *env, jobject thiz, jint fd) {
    // 关闭设备节点
    close(fd);
    LOGD("Device closed\n");

}

在这里插入图片描述 GPIO引脚控制LED调试信息 在这里插入图片描述 在这里插入图片描述

寄存器控制LED调试信息 在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

五、总结与进阶

通过本文,你学会了如何编写一个完整的 Linux 字符设备驱动来控制 LED。驱动开发是一个复杂的领域,后续还可以深入学习:

设备树的使用与驱动适配 中断处理在驱动中的应用 更复杂的设备控制逻辑