摘要
本文记录了在i.MX6ULL嵌入式平台上进行Linux字符设备驱动开发的学习过程,内容涵盖LED、蜂鸣器及按键三类基础外设的驱动实现。文章首先回顾了Linux设备驱动与硬件系统的关系,随后以混杂设备(misc)框架为核心,结合设备树(Device Tree)与GPIO子系统,分别阐述了输出型设备(LED、蜂鸣器)与输入型设备(按键)的驱动设计方法。文中针对实际调试过程中遇到的gpio_direction_input参数错误、设备树节点匹配失败等问题进行了分析,并给出了正确的实现方案。本文可作为嵌入式Linux驱动入门的参考笔记。
1 引言
在嵌入式Linux系统中,设备驱动是连接硬件与操作系统内核的桥梁。根据正点原子i.MX6ULL开发板的驱动课程资料,字符设备驱动是最为基础的一类驱动,其通过open、read、write、close等系统调用为用户空间提供设备访问接口。i.MX6ULL平台采用设备树描述硬件资源,并通过pinctrl与GPIO子系统对引脚进行统一管理,这为驱动的可移植性与规范性提供了保障。
本文基于实际代码调试过程,总结LED、蜂鸣器(BEEP)及按键(KEY)三类驱动的开发要点,并重点分析按键驱动从"误用为输出"到"正确实现输入检测"的修正过程。
2 驱动框架选择:混杂设备与字符设备
根据Linux内核的分类,自定义的简单字符设备通常归入主设备号为10的misc类。混杂设备框架封装了cdev的创建、设备号分配及sysfs节点生成等细节,开发者仅需填充struct miscdevice结构体并调用misc_register即可完成设备注册,极大简化了驱动入口代码。
在LED、蜂鸣器及按键的驱动中,均采用该框架,其基本模板如下:
static struct miscdevice xxx_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "xxx_dev",
.fops = &xxx_fops,
};
static int __init xxx_init(void)
{
misc_register(&xxx_misc);
/* 设备树解析与GPIO初始化 */
return 0;
}
该框架的使用避免了手动调用alloc_chrdev_region、cdev_init等函数,降低了出错概率。
3 设备树与GPIO子系统的配合
i.MX6ULL的引脚功能由IOMUX控制器配置。在设备树中,首先通过pinctrl节点定义引脚的复用功能及电气属性,然后在具体设备节点中引用该配置,并通过gpio属性声明所使用的GPIO编号及有效电平。
以按键为例,设备树节点如下:
putekey {
compatible = "gpio-keys-polled";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
gpio-key = <&gpio1 18 GPIO_ACTIVE_LOW>;
status = "okay";
};
驱动中通过of_find_node_by_path获取节点,再使用of_get_named_gpio解析GPIO编号,最后调用devm_gpio_request_one申请GPIO资源并设置方向。
4 输出型驱动:LED与蜂鸣器
LED与蜂鸣器均属于GPIO输出设备,其驱动逻辑基本相同:在write函数中接收用户空间传入的控制指令,通过gpio_set_value改变引脚电平。
4.1 关键实现
static ssize_t led_write(struct file *fp, const char __user *buf, size_t len, loff_t *off)
{
int value;
if (copy_from_user(&value, buf, sizeof(int)))
return -EFAULT;
gpio_set_value(led_gpio, value ? 0 : 1); // 低电平点亮
return len;
}
4.2 注意事项
-
电平有效极性的处理应与设备树中
GPIO_ACTIVE_LOW或GPIO_ACTIVE_HIGH保持一致。 -
使用
devm_gpio_request_one可自动管理资源释放,避免卸载驱动时遗漏gpio_free。
5 输入型驱动:按键检测的修正过程
按键驱动与LED有本质区别------其GPIO方向为输入,驱动需被动响应引脚电平变化,而非主动控制输出。初次编写时,作者误将按键GPIO配置为输出,并在write函数中调用gpio_set_value,导致编译错误与功能异常。
5.1 错误分析
原始代码中存在两处典型错误:
错误一:gpio_direction_input参数多余
gpio_direction_input(gpiokeynum, 1); // 编译错误
该函数原型为int gpio_direction_input(unsigned gpio),仅接受一个参数。此处误与gpio_direction_output混淆。
错误二:驱动逻辑与设备类型不符
按键作为输入设备,其文件操作应为read而非write。原驱动中仅实现了write并在其中设置GPIO输出值,违背了硬件本意。
5.2 正确实现
修正后的按键驱动read函数如下:
static ssize_t key_read(struct file *fp, char __user *buf, size_t len, loff_t *off)
{
int state = gpio_get_value(key_gpio);
char val = state ? '1' : '0';
if (copy_to_user(buf, &val, 1))
return -EFAULT;
return 1;
}
在初始化阶段,采用devm_gpio_request_one一步完成申请与输入方向设置:
ret = devm_gpio_request_one(dev, key_gpio, GPIOF_IN, "key");
通过上述修正,应用层调用read即可获取按键当前状态,并可结合轮询或中断机制实现按键事件的实时响应。
6 应用层测试程序
用户空间测试程序通过open打开设备节点,循环调用read获取按键状态并打印,其核心代码如下:
fd = open("/dev/key_misc", O_RDONLY);
while (1) {
char val;
read(fd, &val, 1);
printf("Key %s\n", val == '0' ? "Pressed" : "Released");
usleep(100000);
}
对于LED或蜂鸣器,则通过write发送控制命令,例如写入1点亮、0熄灭。
7 调试与问题解决
在驱动开发过程中,遇到的主要问题及解决方案归纳如下:
-
编译错误:
too many arguments to function 'gpio_direction_input'原因:函数调用时多传入了一个参数。
解决:移除多余参数,或改用
devm_gpio_request_one统一配置。 -
设备节点未生成
原因:
misc_register失败或设备树节点compatible属性与驱动不匹配(若采用platform_driver模式)。解决:检查内核日志,确认
/dev/下是否生成对应节点,并核对设备树status属性是否为"okay"。 -
GPIO申请失败
原因:设备树中
gpio-key属性格式错误,或GPIO已被其他驱动占用。解决:使用
gpio_is_valid判断返回值,并通过cat /sys/kernel/debug/gpio查看GPIO占用情况。