字符设备驱动---自己实现点LED

一、修改后代码:直接赋值寄存器控制 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]直接判断,逻辑和第二个代码完全一致,学习成本低。

三、测试方法(和第二个代码完全一样)

  1. 编译驱动 :写 Makefile(指定内核路径),编译出simple_led.ko
  2. 加载驱动depmodmodprobe simple_led(自动生成/dev/simple_led);
  3. 控制 LED
    • 开灯:echo 1 > /dev/simple_led
    • 关灯:echo 0 > /dev/simple_led
  4. 查看日志dmesg | grep LED,能看到亮灭对应的寄存器值和提示。

完整执行步骤(开发板终端输入)

  1. 前置:关闭系统心跳灯干扰(开发板默认 LED 是心跳灯,先临时关闭)

    echo none > /sys/class/leds/work/trigger

  2. 分步执行寄存器配置指令(每步执行后无报错即生效) 以下指令有用

    步骤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时:

  1. 系统会调用驱动的led_write函数;
  2. 驱动通过copy_from_user获取到用户空间传来的1
  3. 驱动向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;
}

成功

相关推荐
Full Stack Developme15 小时前
linux sudo su su - 三者区别
linux·运维·服务器
Byron Loong15 小时前
【系统】Linux内核和发行版的关系
linux·运维·服务器
SmartRadio15 小时前
在CH585M代码中如何精细化配置PMU(电源管理单元)和RAM保留
linux·c语言·开发语言·人工智能·单片机·嵌入式硬件·lora
济61716 小时前
linux(第十四期)--Uboot移植(2)-- 在U-Boot 中添加自己的开发板-- Ubuntu20.04
linux·运维·服务器
ben9518chen16 小时前
嵌入式linux操作系统简介
linux·运维·服务器
菜鸟笔记本16 小时前
linux设置定时备份mysql数据
linux·mysql·oracle
majingming12316 小时前
ubuntu下的交叉编译
linux·运维·ubuntu
shchojj16 小时前
ubuntu 因为写错pam.d文件引起的sudo权限丢失
linux·运维·ubuntu
funfan051716 小时前
【运维】Linux/Debian系统时间校准方法简记
linux·运维·debian