LED Platform 驱动开发 文章封面

LED Platform 驱动开发

硬件:iMX6ULL ALPHA 开发板,LED 连接 GPIO1_IO03,低电平点亮
内核:Linux 4.14
目标:以 platform driver 框架实现 LED 字符设备驱动,支持 write 控制和定时器闪烁


一、硬件说明

参数值
引脚GPIO1_IO03
有效电平低电平点亮
控制寄存器CCM_CCGR1(时钟)、SW_MUX、SW_PAD、GPIO1_DR、GPIO1_GDIR

二、整体架构

flowchart TD
    A[DTS: alientek,led 节点] --> B[platform_device 自动生成]
    C[platform_driver 注册] --> D{of_match_table 匹配}
    B --> D
    D --> E[probe 被调用]
    E --> F[devm_kzalloc 分配 led_dev]
    F --> G[of_iomap 映射5个寄存器]
    G --> H[led_hw_init 初始化硬件]
    H --> I[alloc_chrdev_region 申请设备号]
    I --> J[cdev_add 注册字符设备]
    J --> K[class_create + device_create]
    K --> L["自动创建 /dev/led"]
    L --> M[init_timer 初始化定时器]

三、数据结构关系

classDiagram
    class led_dev {
        +void* ccm_ccgr1
        +void* sw_mux
        +void* sw_pad
        +void* gpio_dr
        +void* gpio_gdir
        +dev_t dev_id
        +cdev cdev
        +class* class
        +device* device
        +timer_list timer
        +spinlock_t lock
        +int timeperiod
        +unsigned char status
    }

    class file {
        +private_data
        +f_inode
    }

    class inode {
        +i_cdev
    }

    class cdev {
        +ops: file_operations*
    }

    class file_operations {
        +open()
        +write()
        +unlocked_ioctl()
    }

    class platform_device {
        +driver_data
        +dev.of_node
    }

    platform_device --> led_dev : "platform_set_drvdata"
    file --> led_dev : "private_data"
    file --> inode : "f_inode"
    inode --> cdev : "i_cdev"
    led_dev *-- cdev : 包含
    cdev --> file_operations : "ops"

关键链路说明:

  • probe 阶段:platform_device → platform_set_drvdata → led_dev,驱动通过 platform_get_drvdata 取回
  • open 阶段:inode->i_cdev → container_of → led_dev → filp->private_data
  • write/ioctl 阶段:filp->private_data → led_dev → gpio_dr → 硬件

四、调用时序

4.1 open → write 流程

sequenceDiagram
    用户程序->>内核VFS: open("/dev/led")
    内核VFS->>led_open: 调用
    led_open->>led_open: container_of(inode->i_cdev)获取led_dev
    led_open->>led_open: filp->private_data = dev
    led_open-->>用户程序: 返回 fd

    用户程序->>内核VFS: write(fd, &val, 1)
    内核VFS->>led_write: 调用
    led_write->>led_write: filp->private_data 取出 led_dev
    led_write->>led_write: copy_from_user 获取控制值
    led_write->>硬件寄存器: writel(gpio_dr) 控制电平
    led_write-->>用户程序: 返回

4.2 定时器闪烁流程

sequenceDiagram
    用户程序->>内核VFS: ioctl(fd, LED_OPEN_TIMER)
    内核VFS->>led_ioctl: 调用
    led_ioctl->>内核定时器: mod_timer 启动

    loop 每隔 timeperiod ms
        内核定时器->>led_timer_cb: 回调触发
        led_timer_cb->>led_timer_cb: status 取反
        led_timer_cb->>硬件寄存器: writel 翻转电平
        led_timer_cb->>内核定时器: mod_timer 重新启动
    end

    用户程序->>内核VFS: ioctl(fd, LED_CLOSE_TIMER)
    内核VFS->>led_ioctl: 调用
    led_ioctl->>内核定时器: del_timer_sync 停止

五、DTS 配置

/* 在 imx6ull-alientek-emmc.dts 中添加 */
&iomuxc {
    pinctrl_led: ledgrp {
        fsl,pins = <
            MX6UL_PAD_GPIO1_IO03__GPIO1_IO03  0x10b0
        >;
    };
};

led_ctrl {
    compatible = "alientek,led";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_led>;
    reg = <0x020C406C 0x04    /* SW_MUX_GPIO1_IO03 */
           0x0209C000 0x04    /* GPIO1_DR          */
           0x0209C004 0x04    /* GPIO1_GDIR        */
           0x020E02F0 0x04    /* SW_PAD_GPIO1_IO03 */
           0x020C4068 0x04>;  /* CCM_CCGR1         */
    status = "okay";
};

of_iomap(np, index) 按 reg 属性中的顺序(0~4)逐个映射,索引顺序与代码中访问顺序对应。


六、关键代码说明

6.1 probe:资源管理

static int led_probe(struct platform_device *pdev)
{
    struct led_dev *dev;

    /* devm_ 系列:设备卸载时自动释放,无需手动 free */
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);

    /* 映射寄存器,错误路径统一 goto err_iomap */
    dev->sw_mux    = of_iomap(np, 0);
    dev->gpio_dr   = of_iomap(np, 1);
    ...

    /* 将 led_dev 绑定到 platform_device,remove 时可取回 */
    platform_set_drvdata(pdev, dev);
}

6.2 open:container_of 获取设备结构体

static int led_open(struct inode *inode, struct file *filp)
{
    /*
     * inode->i_cdev 指向 led_dev.cdev
     * container_of 通过成员地址反推外层结构体首地址
     */
    struct led_dev *dev = container_of(inode->i_cdev,
                                       struct led_dev, cdev);
    filp->private_data = dev;
    return 0;
}

6.3 定时器:spinlock 保护共享数据

/* ioctl 修改 timeperiod(进程上下文) */
spin_lock_irqsave(&dev->lock, flags);
dev->timeperiod = (int)arg;
spin_unlock_irqrestore(&dev->lock, flags);

/* 定时器回调读取 timeperiod(软中断上下文) */
spin_lock_irqsave(&dev->lock, flags);
period = dev->timeperiod;
spin_unlock_irqrestore(&dev->lock, flags);

timeperiod 在进程上下文(ioctl)和软中断上下文(定时器回调)中并发访问,必须用 spin_lock_irqsave 保护。

6.4 ioctl 命令字定义

#define LED_CLOSE_TIMER  _IO(0XEF, 0x1) /* 停止闪烁 */
#define LED_OPEN_TIMER   _IO(0XEF, 0x2) /* 开始闪烁 */
#define LED_SET_PERIOD   _IO(0XEF, 0x3) /* 设置闪烁周期(ms)*/

_IO(type, nr):无数据传递的 ioctl 命令字,type 为幻数(区分不同驱动),nr 为命令编号。


七、错误处理设计

probe 中使用 goto 链,确保出错时按逆序释放已申请的资源:

err_class:   class_destroy
err_cdev:    cdev_del
err_chrdev:  unregister_chrdev_region
err_iomap:   iounmap × 5

常见 Bug:原始版本在错误路径中对全局静态变量执行 kfree,会导致内核 BUG。
重构后改用 devm_kzalloc,内存由内核自动管理,错误路径只需 iounmap 寄存器。


八、用户态测试程序

测试程序位于 app/led_test/led_test.c,支持子命令:

led_test on          # 点亮
led_test off         # 熄灭
led_test blink 500   # 500ms 闪烁
led_test stop        # 停止闪烁

交叉编译:

cd app/led_test
make
cp led_test /nfsroot/usr/bin/


九、调试记录:实际测试踩坑

9.1 Makefile KDIR 路径错误

现象:make 报错 No such file or directory

原因:Makefile 中 KERNEL_DIR 写成了 ../../kernel,实际目录名为 kernel4.14。

修复:

KERNEL_DIR := $(CURRENT_DIR)/../../kernel4.14


9.2 insmod 报 Operation not permitted

现象:

insmod: can't insert 'led.ko': Operation not permitted

排查过程:

  • strings led.ko | grep vermagic → 版本字符串匹配,不是 vermagic 问题
  • cat /proc/sys/kernel/modules_disabled → 0,不是禁用问题
  • cat /sys/module/module/parameters/sig_enforce → N,不是签名问题
  • 从 /tmp 加载仍然失败,排除 NFS 限制

根本原因:led.ko 编译时 KDIR 指向错误路径,编译出的是旧版本产物(甚至是之前遗留的 .ko);修正 KDIR 后重新 make clean && make 问题消失。


9.3 insmod 成功但系统卡死

现象:insmod 后系统立即失去响应,串口无输出。

原因:DTS reg 属性中寄存器地址填错,sw_mux(index 0)被赋值为 CCM_CCGR1 的地址,导致 led_hw_init 执行 writel(0x5, dev->sw_mux) 时实际写入了 CCM_CCGR1,将大量外设时钟关闭,系统挂死。

正确地址对照表(iMX6ULL,GPIO1_IO03):

寄存器正确地址错误地址
SW_MUX_GPIO1_IO030x020E00680x020C406C(CCM_CCGR1!)
GPIO1_DR0x0209C000✅
GPIO1_GDIR0x0209C004✅
SW_PAD_GPIO1_IO030x020E02F40x020E02F0(差4字节)
CCM_CCGR10x020C406C0x020C4068(CCM_CCGR0!)

修复后的 DTS reg 属性:

reg = <0x020E0068 0x04    /* SW_MUX_GPIO1_IO03 */
       0x0209C000 0x04    /* GPIO1_DR          */
       0x0209C004 0x04    /* GPIO1_GDIR        */
       0x020E02F4 0x04    /* SW_PAD_GPIO1_IO03 */
       0x020C406C 0x04>;  /* CCM_CCGR1         */


9.4 probe 失败:pin already requested by tsc

现象:

pin MX6UL_PAD_GPIO1_IO03 already requested by 2040000.tsc
alientek-led: probe of 20c406c.led_ctrl failed with error -22

原因:DTS led_ctrl 节点配置了 pinctrl-0,内核通过 pinctrl 子系统申请 GPIO1_IO03,但该引脚已被触摸屏控制器(TSC)占用。

修复:去掉 led_ctrl 节点中的 pinctrl-names 和 pinctrl-0。我们的驱动通过 of_iomap + writel 直接操作 MUX 寄存器,无需 pinctrl 参与。


9.5 加载成功后的正常提示

led: loading out-of-tree module taints kernel.
alientek-led 20e0068.led_ctrl: LED driver probed

第一行是内核警告,表示加载了非官方树外模块(taint),属于正常现象,不影响功能。第二行确认 probe 成功,/dev/led 设备节点自动生成。


关联笔记

  • 平台设备驱动模型 — platform driver 完整框架讲解
  • iMX6ULL环境数据采集站 — 本驱动是采集站项目第一阶段

延伸阅读

LED 驱动是理解 Platform 模型和 GPIO 生命周期的直接实践。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部