一、修改后代码:直接赋值寄存器控制 LED 亮灭(更直观易懂)
基于第二个代码的结构体封装风格,结合 GPIO0_C0 的硬件特性(控制寄存器为GPIO0_SWPORT_DR_H,地址0xFDD60004),修改后代码直接通过赋值寄存器值控制亮灭,逻辑更直观,核心是固定亮灭对应的寄存器值,避免复杂位运算:
cpp
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/io.h>
// 1. 硬件参数:RK3568 GPIO0_C0完整寄存器配置(与手动测试一致)
#define PMU_GRF_BASE (0xFDC20000) // PMU_GRF基地址
#define PMU_GRF_GPIO0C_IOMUX_L (PMU_GRF_BASE + 0x0010) // 引脚复用寄存器
#define PMU_GRF_GPIO0C_DS_0 (PMU_GRF_BASE + 0x0090) // 驱动能力寄存器
#define GPIO0_BASE (0xFDD60000) // GPIO0基地址
#define GPIO0_SWPORT_DR_H (GPIO0_BASE + 0x0004) // 电平控制寄存器(C0对应)
#define GPIO0_SWPORT_DDR_H (GPIO0_BASE + 0x000C) // 方向控制寄存器(C0对应)
// 关键:修正后的亮灭寄存器值(写使能位bit16=1,而非原代码bit31)
#define LED_ON_VAL 0x00010001 // 开灯:bit16=1(写使能),bit0=1(高电平点亮)
#define LED_OFF_VAL 0x00010000 // 关灯:bit16=1(写使能),bit0=0(低电平熄灭)
#define LED_NAME "simple_led"// 设备名
// 2. 设备结构体(封装所有资源,含PMU寄存器映射指针)
struct led_dev {
dev_t dev_num; // 设备号
struct cdev cdev; // 字符设备核心
struct class *class; // 自动创建设备节点用
struct device *device; // 设备实例
char kbuf[1]; // 接收用户指令(1=亮,0=灭)
void __iomem *vir_gpio_dr; // 电平寄存器虚拟地址
void __iomem *vir_pmu_iomux; // 引脚复用寄存器虚拟地址
void __iomem *vir_pmu_ds; // 驱动能力寄存器虚拟地址
};
struct led_dev dev_led; // 全局设备实例
// 3. open函数:绑定私有数据
static int led_open(struct inode *inode, struct file *filp) {
filp->private_data = &dev_led;
printk("LED device opened!\n");
return 0;
}
// 4. write函数:核心控制(用修正后的寄存器值)
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *off) {
struct led_dev *dev = (struct led_dev *)filp->private_data;
int ret;
// 从用户空间拷贝指令(1字节)
ret = copy_from_user(dev->kbuf, buf, cnt);
if (ret < 0) {
printk("copy_from_user failed!\n");
return -EFAULT;
}
// 直接赋值寄存器控制亮灭(与手动测试指令完全一致)
if (dev->kbuf[0] == '1') {
*(dev->vir_gpio_dr) = LED_ON_VAL;
printk("LED ON (reg: 0x%x)\n", LED_ON_VAL);
} else if (dev->kbuf[0] == '0') {
*(dev->vir_gpio_dr) = LED_OFF_VAL;
printk("LED OFF (reg: 0x%x)\n", LED_OFF_VAL);
}
return cnt;
}
// 5. release函数:空实现
static int led_release(struct inode *inode, struct file *filp) {
printk("LED device released!\n");
return 0;
}
// 6. 文件操作集合
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.release = led_release,
};
// 7. 驱动入口:完整硬件初始化+设备注册
static int __init led_init(void) {
int ret;
u32 val; // 临时存储寄存器值
// 步骤1:动态分配设备号
ret = alloc_chrdev_region(&dev_led.dev_num, 0, 1, LED_NAME);
if (ret < 0) {
printk("alloc_chrdev_region failed!\n");
goto err_alloc;
}
// 步骤2:初始化cdev并添加到内核
cdev_init(&dev_led.cdev, &led_fops);
dev_led.cdev.owner = THIS_MODULE;
ret = cdev_add(&dev_led.cdev, dev_led.dev_num, 1);
if (ret < 0) {
printk("cdev_add failed!\n");
goto err_cdev;
}
// 步骤3:创建类和设备(自动生成/dev/simple_led)
dev_led.class = class_create(THIS_MODULE, LED_NAME);
if (IS_ERR(dev_led.class)) {
ret = PTR_ERR(dev_led.class);
goto err_class;
}
dev_led.device = device_create(dev_led.class, NULL, dev_led.dev_num, NULL, LED_NAME);
if (IS_ERR(dev_led.device)) {
ret = PTR_ERR(dev_led.device);
goto err_device;
}
// 步骤4:映射所有需要的寄存器(含PMU和GPIO)
// 4.1 映射PMU寄存器(引脚复用+驱动能力)
dev_led.vir_pmu_iomux = ioremap(PMU_GRF_GPIO0C_IOMUX_L, 4);
dev_led.vir_pmu_ds = ioremap(PMU_GRF_GPIO0C_DS_0, 4);
// 4.2 映射GPIO寄存器(方向+电平)
dev_led.vir_gpio_dr = ioremap(GPIO0_SWPORT_DR_H, 4);
void __iomem *vir_gpio_ddr = ioremap(GPIO0_SWPORT_DDR_H, 4);
// 检查映射是否失败
if (IS_ERR(dev_led.vir_pmu_iomux) || IS_ERR(dev_led.vir_pmu_ds) ||
IS_ERR(dev_led.vir_gpio_dr) || IS_ERR(vir_gpio_ddr)) {
ret = PTR_ERR(dev_led.vir_pmu_iomux);
goto err_ioremap;
}
// 步骤5:硬件配置(与手动测试指令完全一致)
// 5.1 配置GPIO0_C0为GPIO功能(解除复用)
val = 0x00070000; // bit18:16=111(写使能),bit2:0=000(GPIO功能)
writel(val, dev_led.vir_pmu_iomux);
// 5.2 配置驱动能力为Level5(增强LED亮度)
val = 0x003F003F; // bit21:16=111111(写使能),bit5:0=111111(Level5)
writel(val, dev_led.vir_pmu_ds);
// 5.3 配置GPIO0_C0为输出模式
val = 0x00010001; // bit16=1(写使能),bit0=1(输出模式)
writel(val, vir_gpio_ddr);
// 5.4 默认关闭LED
writel(LED_OFF_VAL, dev_led.vir_gpio_dr);
// 步骤6:释放临时映射的方向寄存器(配置完无需保留)
iounmap(vir_gpio_ddr);
// 释放PMU寄存器映射(配置完无需保留)
iounmap(dev_led.vir_pmu_iomux);
iounmap(dev_led.vir_pmu_ds);
printk("LED driver init success! (major: %d)\n", MAJOR(dev_led.dev_num));
return 0;
// 错误处理:按逆序释放资源
err_ioremap:
iounmap(dev_led.vir_pmu_iomux);
iounmap(dev_led.vir_pmu_ds);
iounmap(dev_led.vir_gpio_dr);
iounmap(vir_gpio_ddr);
device_destroy(dev_led.class, dev_led.dev_num);
err_device:
class_destroy(dev_led.class);
err_class:
cdev_del(&dev_led.cdev);
err_cdev:
unregister_chrdev_region(dev_led.dev_num, 1);
err_alloc:
return ret;
}
// 8. 驱动出口:释放资源
static void __exit led_exit(void) {
// 释放电平寄存器映射
iounmap(dev_led.vir_gpio_dr);
// 注销设备节点、类、cdev、设备号
device_destroy(dev_led.class, dev_led.dev_num);
class_destroy(dev_led.class);
cdev_del(&dev_led.cdev);
unregister_chrdev_region(dev_led.dev_num, 1);
printk("LED driver exit!\n");
}
// 模块加载/卸载宏
module_init(led_init);
module_exit(led_exit);
// 模块信息(必须添加GPL协议)
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("RK3568 LED Driver (Correct Reg Config)");
二、关键修改说明(为什么更易理解)
1. 核心逻辑简化:直接赋值寄存器值
- 原来的代码用
readl读寄存器→改 bit→writel写回,需要理解位运算; - 现在直接定义
LED_ON_VAL(0x80000001)和LED_OFF_VAL(0x80000000),值的含义直观:bit16=1:寄存器写使能(必须置 1 才能修改电平);bit0=1:GPIO0_C0 输出高电平(亮);bit0=0:输出低电平(灭);- 高 16 位
0x8000:保留位(芯片要求,直接复用无需理解细节)。
2. 硬件配置简化:关键步骤可视化
- 初始化时直接配置 GPIO 为输出:映射方向寄存器→写固定值
0x80000001→释放映射,步骤少且直观; - 控制寄存器
vir_gpio_dr直接对应GPIO0_SWPORT_DR_H,变量名见名知意,无需记多个寄存器指针。
3. 用户接口不变:还是传 1/0 控制
- 用户 APP 仍通过
write向/dev/simple_led写入 1(亮)或 0(灭),无需改应用代码; - 驱动中
kbuf[0]直接判断,逻辑和第二个代码完全一致,学习成本低。
三、测试方法(和第二个代码完全一样)
- 编译驱动 :写 Makefile(指定内核路径),编译出
simple_led.ko; - 加载驱动 :
depmod→modprobe simple_led(自动生成/dev/simple_led); - 控制 LED :
- 开灯:
echo 1 > /dev/simple_led; - 关灯:
echo 0 > /dev/simple_led;
- 开灯:
- 查看日志 :
dmesg | grep LED,能看到亮灭对应的寄存器值和提示。
完整执行步骤(开发板终端输入)
-
前置:关闭系统心跳灯干扰(开发板默认 LED 是心跳灯,先临时关闭)
echo none > /sys/class/leds/work/trigger
-
分步执行寄存器配置指令(每步执行后无报错即生效) 以下指令有用
步骤1:配置GPIO0_C0为纯GPIO功能(解除复用)
devmem 0xFDC20010 w 0x00070000
步骤2:配置GPIO0_C0驱动能力为Level5(确保LED亮度足够)
devmem 0xFDC20090 w 0x003F003F
步骤3:配置GPIO0_C0为输出模式(关键!不配置输出无法控制电平)
devmem 0xFDD6000C w 0x00010001
步骤4:控制LED亮(高电平)
devmem 0xFDD60004 w 0x00010001
步骤5:控制LED灭(低电平)
devmem 0xFDD60004 w 0x00010000 # 测试亮后再执行此指令灭灯

执行 devmem 0xFDD60004 w 0x00010001

执行 devmem 0xFDD60004 w 0x00010000

自己写的驱动代码设备节点 /dev/simple_led
控制:echo 1 > /dev/simple_led(开灯)、echo 0 > /dev/simple_led(关灯)
这个指令是在用户空间通过驱动的设备节点控制 LED 点亮,拆解每部分的含义:
1. 指令各部分作用
echo 1:在终端输出字符串1(这里的1是我们驱动约定的 "开灯指令");>:是 Linux 的输出重定向符号 ,把前面echo输出的内容,写到后面的 "文件" 里;/dev/simple_led:不是普通文件,是我们驱动自动创建的字符设备节点(内核通过它实现 "用户空间←→驱动" 的交互)。
2. 指令和驱动的关联逻辑
当执行echo 1 > /dev/simple_led时:
- 系统会调用驱动的
led_write函数; - 驱动通过
copy_from_user获取到用户空间传来的1; - 驱动向
GPIO0_SWPORT_DR_H寄存器写入LED_ON_VAL(0x00010001),最终控制 LED 点亮。
简单说:这个指令就是给驱动发 "开灯" 指令 ,对应的 "关灯" 指令是echo 0 > /dev/simple_led。

成功打开关闭了led
再写应用程序
cpp
#include <stdio.h> // 标准输入输出库
#include <stdlib.h> // 标准库
#include <sys/types.h> // 系统数据类型定义
#include <sys/stat.h> // 文件状态相关定义
#include <fcntl.h> // 文件控制相关函数(open等)
#include <unistd.h> // 系统调用(close、write等)
// 主函数:通过命令行参数控制LED(参数1:输入"1"=开灯,"0"=关灯,对应ASCII码49/48)
int main(int argc, char *argv[])
{
int fd; // 文件描述符(用于操作设备节点)
char buf[32] = {0}; // 数据缓冲区(仅用buf[0]存储控制指令)
// 1. 校验命令行参数数量(需传入2个参数:程序名 + "1"/"0")
if (argc != 2)
{
printf("Usage: %s <1|0>\n", argv[0]);
printf(" 1: Turn LED ON (对应ASCII码49)\n");
printf(" 0: Turn LED OFF (对应ASCII码48)\n");
return -1;
}
// 2. 打开LED驱动设备节点(路径与驱动创建的节点一致:/dev/simple_led)
fd = open("/dev/simple_led", O_RDWR);
if (fd < 0) // 检查设备是否成功打开
{
perror("open /dev/simple_led error"); // 打印错误信息
return fd; // 打开失败,返回错误码
}
// 3. 处理命令行参数:直接取参数的第一个字符("1"→'1',"0"→'0',对应ASCII码49/48)
// 无需atoi转换,避免将字符"1"转为数字1(字节值1,驱动无法识别)
buf[0] = argv[1][0];
// 4. 向驱动写入控制指令(仅传递1字节有效数据:字符'1'或'0')
write(fd, buf, sizeof(buf[0]));
// 5. 关闭设备节点,释放资源
close(fd);
printf("Command sent: %c (ASCII: %d)\n", buf[0], (unsigned char)buf[0]);
return 0;
}

成功