嵌入式Linux驱动开发:蜂鸣器驱动
1. 引言
本文档详细记录了基于i.MX6ULL处理器的蜂鸣器驱动开发过程。内容涵盖驱动的理论基础、代码实现、设备树配置以及用户空间应用程序的编写。本文档严格遵循用户提供的代码和文档,确保理论与实践的紧密结合。本文档旨在为嵌入式Linux驱动开发者提供一份详尽的参考,帮助理解平台驱动模型、设备树的使用以及字符设备驱动的实现。
2. 理论基础
2.1 Linux驱动模型
Linux内核提供了多种驱动模型,以适应不同的硬件设备。其中,平台驱动模型(Platform Driver Model)是针对SoC(System on Chip)内部集成的外设而设计的。这些外设通常与CPU紧密耦合,其资源(如内存映射、中断)在系统启动时就已经确定。
平台驱动模型的核心思想是将设备和驱动分离,通过设备树(Device Tree)来描述硬件信息。设备树在系统启动时被加载,内核根据设备树中的节点信息来创建平台设备(platform_device
),然后平台总线(platform_bus
)负责匹配设备和驱动。
2.2 平台驱动模型
平台驱动模型由三个主要部分组成:平台设备(platform_device
)、平台驱动(platform_driver
)和平台总线(platform_bus
)。
- 平台设备(
platform_device
):代表一个物理设备。它包含了设备的资源信息,如内存地址、中断号等。在旧的内核版本中,这些信息通常在内核启动时静态注册。在现代内核中,这些信息通常由设备树提供。 - 平台驱动(
platform_driver
) :代表一个驱动程序。它包含了驱动的初始化、卸载、探测(probe)和移除(remove)等函数。驱动通过platform_driver_register
函数注册到平台总线上。 - 平台总线(
platform_bus
) :负责匹配设备和驱动。当一个平台设备被注册时,平台总线会遍历所有已注册的平台驱动,通过of_match_table
或name
字段进行匹配。如果匹配成功,则调用驱动的probe
函数。
2.3 设备树(Device Tree)
设备树是一种描述硬件的文本文件,它使用一种树形结构来组织信息。设备树文件(.dts
)在编译时被转换为设备树二进制文件(.dtb
),并在系统启动时被加载到内存中。
设备树的主要优点是:
- 硬件描述与驱动代码分离:驱动代码不再需要硬编码硬件信息,使得驱动更具可移植性。
- 动态配置:可以通过修改设备树文件来改变硬件配置,而无需重新编译内核。
在设备树中,每个节点代表一个设备或子系统。节点的属性(properties)描述了设备的特性。例如,compatible
属性用于匹配驱动,reg
属性描述了设备的内存映射地址,interrupts
属性描述了设备的中断号。
2.4 字符设备驱动
字符设备是Linux中最基本的设备类型之一,它以字节流的形式进行数据传输。字符设备驱动的核心是file_operations
结构体,它定义了驱动支持的操作,如open
、read
、write
、release
等。
在Linux内核中,每个字符设备都有一个主设备号和一个次设备号。主设备号标识设备类型,次设备号标识具体的设备实例。设备文件通常位于/dev
目录下。
2.5 杂项设备(Miscellaneous Device)
杂项设备是一种特殊的字符设备,它使用一个固定的主设备号(10),次设备号范围为0到255。杂项设备的目的是简化字符设备的注册过程。通过使用misc_register
函数,驱动可以避免手动分配设备号和创建设备文件。
3. 代码实现
3.1 平台设备(leddevice.c
)
leddevice.c
文件定义了一个平台设备,该设备用于模拟一个LED。虽然文件名为leddevice.c
,但其内容实际上与蜂鸣器驱动相关。该文件的主要作用是向内核注册一个平台设备,该设备描述了蜂鸣器所需的硬件资源。
c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/string.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/ide.h>
#include <linux/platform_device.h>
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004)
#define REGISTER_LNE 4
- 寄存器物理地址 :这些宏定义了i.MX6ULL处理器中与GPIO相关的寄存器的物理地址。
CCM_CCGR1_BASE
是时钟控制模块的寄存器,用于使能GPIO1的时钟。SW_MUX_GPIO1_IO03_BASE
和SW_PAD_GPIO1_IO03_BASE
分别是复用和电气特性的配置寄存器。GPIO1_DR_BASE
和GPIO1_GDIR_BASE
分别是GPIO数据寄存器和方向寄存器。
c
void leddevice_realease(struct device *dev)
{
printk("Device: leddevice_realease!\r\n");
}
- 设备释放函数:当平台设备被注销时,内核会调用此函数。这是一个空函数,仅用于打印一条调试信息。
c
static struct resource led_resources[] = {
[0] = {
.start = CCM_CCGR1_BASE,
.end = CCM_CCGR1_BASE + REGISTER_LNE - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = SW_MUX_GPIO1_IO03_BASE,
.end = SW_MUX_GPIO1_IO03_BASE + REGISTER_LNE - 1,
.flags = IORESOURCE_MEM,
},
[2] = {
.start = SW_PAD_GPIO1_IO03_BASE,
.end = SW_PAD_GPIO1_IO03_BASE + REGISTER_LNE - 1,
.flags = IORESOURCE_MEM,
},
[3] = {
.start = GPIO1_DR_BASE,
.end = GPIO1_DR_BASE + REGISTER_LNE - 1,
.flags = IORESOURCE_MEM,
},
[4] = {
.start = GPIO1_GDIR_BASE,
.end = GPIO1_GDIR_BASE + REGISTER_LNE - 1,
.flags = IORESOURCE_MEM,
},
};
- 资源数组 :
led_resources
数组定义了平台设备所需的资源。每个资源结构体包含起始地址、结束地址和标志。IORESOURCE_MEM
标志表示这是一个内存资源。这些资源在驱动的probe
函数中被映射到虚拟地址空间。
c
static struct platform_device leddevice = {
.name = "imx6ull-led",
.id = -1,
.dev = {
.release = leddevice_realease,
},
.num_resources = ARRAY_SIZE(led_resources),
.resource = led_resources,
};
- 平台设备结构体 :
leddevice
结构体定义了一个平台设备。.name
字段必须与驱动中的.name
字段匹配,以便平台总线能够正确匹配设备和驱动。.id
字段设置为-1,表示这是唯一的设备实例。.dev.release
字段指定了设备释放函数。.num_resources
和.resource
字段指定了设备的资源。
c
static int __init
leddevice_init(void)
{
platform_device_register(&leddevice);
return 0;
}
static void __exit leddevice_exit(void)
{
platform_device_unregister(&leddevice);
}
module_init(leddevice_init);
module_exit(leddevice_exit);
MODULE_LICENSE("GPL");
- 模块初始化和退出 :
leddevice_init
函数在模块加载时被调用,它注册平台设备。leddevice_exit
函数在模块卸载时被调用,它注销平台设备。module_init
和module_exit
宏用于指定初始化和退出函数。
3.2 蜂鸣器驱动(miscbeep.c
)
miscbeep.c
文件实现了蜂鸣器的驱动程序。该驱动使用平台驱动模型和杂项设备框架。
c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
#include <linux/string.h>
#include <linux/ide.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#define MISCBEEP_NAME "miscbeep"
#define MISCBEEP_MINOR 144
#define BEEP_OFF 0
#define BEEP_ON 1
- 宏定义 :
MISCBEEP_NAME
定义了杂项设备的名称,该名称将出现在/dev
目录下。MISCBEEP_MINOR
定义了次设备号。BEEP_OFF
和BEEP_ON
定义了蜂鸣器的状态。
c
struct miscbeep_dev
{
int beep_gpio;
struct device_node *nd;
};
struct miscbeep_dev miscbeep;
- 设备结构体 :
miscbeep_dev
结构体用于保存驱动的状态信息。beep_gpio
字段保存了蜂鸣器GPIO的编号。nd
字段保存了设备树节点的指针,用于在probe
函数中获取设备信息。
c
static int miscbeep_open(struct inode *inode, struct file *filp)
{
filp->private_data = &miscbeep;
return 0;
}
- open函数 :当用户空间程序打开设备文件时,内核调用此函数。该函数将设备结构体的指针保存在
filp->private_data
中,以便在后续的read
、write
等操作中使用。
c
static ssize_t miscbeep_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *ppos)
{
int retvalue;
unsigned char databuf[1];
unsigned char beepstat;
struct miscbeep_dev *dev = filp->private_data;
retvalue = copy_from_user(databuf, buf, cnt);
if (retvalue < 0)
{
printk("kernel write failed!\r\n");
return -EFAULT;
}
beepstat = databuf[0]; /* 获取状态值 */
if (beepstat == BEEP_ON)
{
gpio_set_value(dev->beep_gpio, 0); /* 打开蜂鸣器 */
}
else if (beepstat == BEEP_OFF)
{
gpio_set_value(dev->beep_gpio, 1); /* 关闭蜂鸣器 */
}
return 0;
}
- write函数 :当用户空间程序向设备文件写入数据时,内核调用此函数。该函数首先使用
copy_from_user
函数将用户空间的数据复制到内核空间。然后,根据写入的数据(0或1)来控制蜂鸣器的开关。gpio_set_value
函数用于设置GPIO的电平。注意,蜂鸣器的控制逻辑是低电平有效,因此写入0表示打开,写入1表示关闭。
c
static int miscbeep_release(struct inode *inode, struct file *filep)
{
return 0;
}
- release函数:当用户空间程序关闭设备文件时,内核调用此函数。该函数目前为空,但在实际应用中,可以用于释放资源或执行清理操作。
c
struct file_operations miscbeep_fops = {
.owner = THIS_MODULE,
.open = miscbeep_open,
.write = miscbeep_write,
.release = miscbeep_release,
};
- 文件操作结构体 :
miscbeep_fops
结构体定义了驱动支持的操作。.owner
字段设置为THIS_MODULE
,表示该结构体属于当前模块。.open
、.write
和.release
字段分别指向相应的函数。
c
struct miscdevice beep_miscdev = {
.minor = MISCBEEP_MINOR,
.name = MISCBEEP_NAME,
.fops = &miscbeep_fops,
};
- 杂项设备结构体 :
beep_miscdev
结构体定义了杂项设备。.minor
字段指定了次设备号。.name
字段指定了设备名称。.fops
字段指向文件操作结构体。
c
static int miscbeep_probe(struct platform_device *dev)
{
int ret = 0;
miscbeep.nd = dev->dev.of_node;
miscbeep.beep_gpio = of_get_named_gpio(miscbeep.nd, "beep-gpios", 0);
if (miscbeep.beep_gpio < 0)
{
ret = -EINVAL;
goto fail_get_gpio;
}
ret = gpio_request(miscbeep.beep_gpio, "beep-gpio");
if (ret)
{
ret = -EINVAL;
goto fail_req;
}
ret = gpio_direction_output(miscbeep.beep_gpio, 1);
if (ret < 0)
{
ret = -EINVAL;
goto fail_gpio_dir;
}
ret = misc_register(&beep_miscdev);
if (ret)
{
ret = -EINVAL;
goto fail_misc_reg;
}
return 0;
fail_misc_reg:
printk("Driver: fail_misc_reg\r\n");
fail_gpio_dir:
gpio_free(miscbeep.beep_gpio);
printk("Driver: fail_gpio_dir\r\n");
fail_req:
printk("Driver: fail_req\r\n");
fail_get_gpio:
printk("Driver: fail_get_gpio\r\n");
return ret;
}
- probe函数 :当平台总线匹配到设备和驱动时,内核调用此函数。该函数执行以下操作:
- 获取设备树节点指针。
- 使用
of_get_named_gpio
函数从设备树中获取蜂鸣器GPIO的编号。 - 使用
gpio_request
函数申请GPIO。 - 使用
gpio_direction_output
函数将GPIO设置为输出模式,并初始化为高电平(关闭蜂鸣器)。 - 使用
misc_register
函数注册杂项设备。
如果任何一步失败,函数将跳转到相应的错误处理标签,释放已申请的资源。
c
static int miscbeep_remove(struct platform_device *dev)
{
misc_deregister(&beep_miscdev);
gpio_set_value(miscbeep.beep_gpio, 1);
gpio_free(miscbeep.beep_gpio);
return 0;
}
- remove函数 :当平台设备被注销时,内核调用此函数。该函数执行以下操作:
- 使用
misc_deregister
函数注销杂项设备。 - 将GPIO设置为高电平,关闭蜂鸣器。
- 使用
gpio_free
函数释放GPIO。
- 使用
c
static struct of_device_id beep_of_match[] = {
{.compatible = "alientek,beep"},
{/* sentinel */},
};
- 设备树匹配表 :
beep_of_match
数组定义了驱动支持的设备。.compatible
字段必须与设备树中的compatible
属性匹配。平台总线使用此表来匹配设备和驱动。
c
struct platform_driver miscbeep_driver = {
.driver = {
.name = "imx6ul-beep",
.of_match_table = beep_of_match,
},
.remove = miscbeep_remove,
.probe = miscbeep_probe,
};
- 平台驱动结构体 :
miscbeep_driver
结构体定义了平台驱动。.driver.name
字段必须与平台设备的.name
字段匹配。.of_match_table
字段指向设备树匹配表。.probe
和.remove
字段分别指向probe
和remove
函数。
c
static int __init
miscbeep_init(void)
{
return platform_driver_register(&miscbeep_driver);
}
static void __exit miscbeep_exit(void)
{
platform_driver_unregister(&miscbeep_driver);
}
module_init(miscbeep_init);
module_exit(miscbeep_exit);
MODULE_LICENSE("GPL");
- 模块初始化和退出 :
miscbeep_init
函数在模块加载时被调用,它注册平台驱动。miscbeep_exit
函数在模块卸载时被调用,它注销平台驱动。
3.3 Makefile
makefile
KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)
obj-m := miscbeep.o
build : kernel_modules
kernel_modules:
$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modules
clean:
$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
- Makefile :该Makefile用于编译驱动模块。
KERNERDIR
变量指定了内核源码的路径。CURRENTDIR
变量指定了当前目录的路径。obj-m
变量指定了要编译的模块文件。kernel_modules
目标调用内核的Makefile
来编译模块。clean
目标用于清理编译生成的文件。
3.4 用户空间应用程序(miscbeepAPP.c
)
c
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#define LEDOFF 0
#define LEDON 1
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
unsigned char databuf[1];
if (argc != 3)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打开led驱动 */
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */
/* 向/dev/led文件写入数据 */
retvalue = write(fd, databuf, sizeof(databuf));
if (retvalue < 0)
{
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
retvalue = close(fd); /* 关闭文件 */
if (retvalue < 0)
{
printf("file %s close failed!\r\n", argv[1]);
return -1;
}
return 0;
}
- 用户空间应用程序:该程序用于控制蜂鸣器。它接受两个命令行参数:设备文件路径和操作(0或1)。程序首先打开设备文件,然后向设备文件写入操作数据,最后关闭设备文件。
4. 设备树配置
4.1 设备树节点
在imx6ull-alientek-emmc.dts
文件中,定义了蜂鸣器的设备树节点:
dts
beep{
compatible = "alientek,beep";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_beep>;
states = "okay";
beep-gpios = <&gpio5 1 GPIO_ACTIVE_HIGH>;
};
- compatible :该属性的值必须与驱动中的
of_device_id
表中的.compatible
字段匹配。 - pinctrl-names 和pinctrl-0 :这些属性指定了引脚控制配置。
pinctrl-0
指向pinctrl_beep
节点,该节点定义了GPIO5_IO01引脚的复用和电气特性。 - beep-gpios :该属性指定了蜂鸣器使用的GPIO。
<&gpio5 1 GPIO_ACTIVE_HIGH>
表示使用GPIO5的第1个引脚,高电平有效。
4.2 引脚控制配置
在iomuxc
节点中,定义了pinctrl_beep
节点:
dts
pinctrl_beep: beepgrp {
fsl,pins = <
MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x10b0
>;
};
- MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01:该宏将SNVS_TAMPER1引脚复用为GPIO5_IO01。
- 0x10b0:该值配置了引脚的电气特性,如驱动强度、上拉/下拉电阻等。
5. 编译和测试
5.1 编译驱动
- 将
miscbeep.c
、Makefile
和leddevice.c
文件复制到开发板的内核源码目录。 - 在
miscbeep
目录下执行make
命令,编译驱动模块。 - 使用
scp
命令将生成的miscbeep.ko
文件复制到开发板。
5.2 加载驱动
-
在开发板上,使用
insmod
命令加载leddevice.ko
模块:bashinsmod leddevice.ko
-
加载
miscbeep.ko
模块:bashinsmod miscbeep.ko
5.3 测试驱动
-
编译用户空间应用程序:
bashgcc -o miscbeepAPP miscbeepAPP.c
-
运行应用程序,打开蜂鸣器:
bash./miscbeepAPP /dev/miscbeep 1
-
运行应用程序,关闭蜂鸣器:
bash./miscbeepAPP /dev/miscbeep 0