前言 在 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:GPIO0
GPIO4,每组又以 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。驱动开发是一个复杂的领域,后续还可以深入学习:
设备树的使用与驱动适配 中断处理在驱动中的应用 更复杂的设备控制逻辑