硬件: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_IO03 | 0x020E0068 | 0x020C406C(CCM_CCGR1!) |
| GPIO1_DR | 0x0209C000 | ✅ |
| GPIO1_GDIR | 0x0209C004 | ✅ |
| SW_PAD_GPIO1_IO03 | 0x020E02F4 | 0x020E02F0(差4字节) |
| CCM_CCGR1 | 0x020C406C | 0x020C4068(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 生命周期的直接实践。
- 返回嵌入式 Linux 学习路线,确认本文在完整知识结构中的位置。
- Linux Platform 设备驱动模型
- GPIO Hog 机制详解




