01-LED与蜂鸣器驱动.md 28 KB


title: LED与蜂鸣器驱动 tags: [Linux驱动, LED, 蜂鸣器, 嵌入式, GPIO, 设备树, 字符设备] created: 2026-09-16 updated: 2026-09-17 pdf_ref:

  • "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十一章 嵌入式Linux LED驱动开发实验"
  • "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十六章 Linux蜂鸣器实验" ---

LED与蜂鸣器驱动

关联知识: [[03-pinctrl与gpio子系统]] | [[02-Linux字符设备驱动框架]] | [[04-设备树基础]]


一、LED驱动概述

1.1 LED硬件电路

I.MX6U-ALPHA开发板上的LED灯连接到I.MX6ULL的GPIO1_IO03引脚,为低电平有效(GPIO输出低电平时LED点亮)。硬件电路核心要素:

项目 说明
控制引脚 GPIO1_IO03
有效电平 低电平有效(Active Low)
驱动方式 GPIO推挽输出
限流电阻 串联在LED回路中

1.2 GPIO控制原理

Linux下的任何外设驱动,最终都是要配置相应的硬件寄存器。LED驱动的本质就是对I.MX6ULL的GPIO口进行配置。涉及的关键寄存器:

寄存器 物理地址 功能
CCM_CCGR1 0X020C406C 时钟使能寄存器
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 0X020E0068 复用功能配置
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 0X020E02F4 IO属性配置
GPIO1_DR 0X0209C000 数据寄存器
GPIO1_GDIR 0X0209C004 方向寄存器

1.3 地址映射与MMU

Linux开启MMU后,CPU访问的都是虚拟地址。物理寄存器地址不能直接操作,必须通过地址映射:

  • ioremap(phys_addr, size):将物理地址映射为虚拟地址
  • iounmap(addr):取消地址映射,释放虚拟地址空间

映射后的读写操作推荐使用内核提供的I/O内存访问函数:

// 读操作
u8 readb(addr);    // 8bit读
u16 readw(addr);   // 16bit读
u32 readl(addr);   // 32bit读

// 写操作
writeb(value, addr);  // 8bit写
writew(value, addr);  // 16bit写
writel(value, addr);  // 32bit写

1.4 LED驱动框架

Linux LED驱动采用字符设备驱动框架,核心结构:

用户空间 APP
    ↓ open/read/write
/dev/led 设备节点
    ↓ file_operations
LED驱动(led_fops)
    ↓ led_switch()
硬件寄存器(GPIO1_IO03)

驱动加载流程:ioremap地址映射使能时钟配置复用设置方向注册字符设备创建设备节点


二、LED设备树配置

📖 章节说明:第四十一章的 LED 驱动采用直接操作寄存器的方式,未使用设备树;本节设备树版本对应《指南》第四十五章《设备树下的 LED 驱动实验》及后续 pinctrl/gpio 子系统章节,属相邻章节补充内容。

2.1 设备树基础概念

设备树(Device Tree)用于描述硬件信息,避免在驱动中硬编码硬件参数。LED设备树配置包含两部分:pinctrl节点和设备节点。

2.2 pinctrl节点

pinctrl节点定义引脚的复用功能和电气属性。在imx6ull-alientek-emmc.dtsiomuxc节点下添加:

pinctrl_led: ledgrp {
    fsl,pins = <
        MX6ULL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED */
    >;
};
属性 说明
fsl,pins 飞思卡尔平台引脚配置属性
MX6ULL_PAD_GPIO1_IO03__GPIO1_IO03 宏定义,将引脚复用为GPIO
0x10B0 IO属性值(驱动强度、上下拉等)

MX6ULL_PAD_GPIO1_IO03__GPIO1_IO03定义在arch/arm/boot/dts/imx6ull-pinfunc.h中。

2.3 GPIO属性

设备节点通过GPIO属性引用pinctrl配置的引脚:

led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
属性 说明
&gpio1 引用gpio1控制器节点
3 GPIO编号(GPIO1_IO03的第3个引脚)
GPIO_ACTIVE_LOW 低电平有效

2.4 完整设备树示例

/* 在根节点"/"下添加 */
gpioled {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "alientek,gpioled";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_led>;
    led-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
    status = "okay";
};
属性 说明
compatible 匹配驱动的字符串,驱动中of_match_table使用
pinctrl-names pinctrl状态名称
pinctrl-0 引用默认状态的pinctrl配置
led-gpios 自定义属性名,驱动中通过of_get_named_gpio获取
status "okay"表示启用该节点

验证设备树是否修改成功:编译设备树(make dtbs)后启动系统,检查/proc/device-tree/gpioled节点是否存在。


三、LED驱动源码分析

3.1 完整源码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define LED_MAJOR   200
#define LED_NAME    "led"
#define LEDOFF      0
#define LEDON       1

/* 寄存器物理地址 */
#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)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

void led_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);
        writel(val, GPIO1_DR);
    }else if(sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val |= (1 << 3);
        writel(val, GPIO1_DR);
    }
}

static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf,
                        size_t cnt, loff_t *offt)
{
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;

    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];
    if(ledstat == LEDON) {
        led_switch(LEDON);
    } else if(ledstat == LEDOFF) {
        led_switch(LEDOFF);
    }
    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

static int __init led_init(void)
{
    int retvalue = 0;
    u32 val = 0;

    /* 1、寄存器地址映射 */
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

    /* 2、使能GPIO1时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);
    val |= (3 << 26);
    writel(val, IMX6U_CCM_CCGR1);

    /* 3、设置GPIO1_IO03复用为GPIO,设置IO属性 */
    writel(5, SW_MUX_GPIO1_IO03);
    writel(0x10B0, SW_PAD_GPIO1_IO03);

    /* 4、设置GPIO1_IO03为输出 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);
    val |= (1 << 3);
    writel(val, GPIO1_GDIR);

    /* 5、默认关闭LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);
    writel(val, GPIO1_DR);

    /* 6、注册字符设备驱动 */
    retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
    if(retvalue < 0){
        printk("register chrdev failed!\r\n");
        return -EIO;
    }
    return 0;
}

static void __exit led_exit(void)
{
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);
    unregister_chrdev(LED_MAJOR, LED_NAME);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

3.2 led_switch函数分析

void led_switch(u8 sta)
{
    u32 val = 0;
    if(sta == LEDON) {
        val = readl(GPIO1_DR);       // 读取GPIO1数据寄存器
        val &= ~(1 << 3);            // 清除bit3,输出低电平(LED亮)
        writel(val, GPIO1_DR);       // 写回寄存器
    }else if(sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val |= (1 << 3);             // 置位bit3,输出高电平(LED灭)
        writel(val, GPIO1_DR);
    }
}
  • val &= ~(1 << 3):将bit3清零,GPIO输出低电平,LED点亮(低电平有效)
  • val |= (1 << 3):将bit3置1,GPIO输出高电平,LED熄灭

3.3 led_init入口函数分析

驱动加载时执行的初始化流程:

  1. 地址映射(147~151行):通过ioremap将5个寄存器的物理地址映射为虚拟地址
  2. 使能时钟(154~157行):设置CCM_CCGR1寄存器的bit[27:26],使能GPIO1时钟
  3. 配置复用(162行):向SW_MUX寄存器写入5,将GPIO1_IO03复用为GPIO功能
  4. 设置IO属性(165行):向SW_PAD寄存器写入0x10B0,配置驱动强度和上下拉
  5. 设置方向(168~171行):将GPIO1_GDIR的bit3置1,配置为输出模式
  6. 初始化电平(174~176行):将GPIO1_DR的bit3置1,默认关闭LED
  7. 注册字符设备(179行):调用register_chrdev注册设备号200,设备名"led"

3.4 led_exit出口函数分析

驱动卸载时执行的清理流程:

  1. 取消地址映射(195~199行):调用iounmap释放5个虚拟地址
  2. 注销字符设备(202行):调用unregister_chrdev释放设备号200

四、LED测试程序

4.1 测试APP源码

#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

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;
}

4.2 调用流程

./ledApp /dev/led 1    // 打开LED
./ledApp /dev/led 0    // 关闭LED

程序执行流程:open("/dev/led")write(fd, &1, 1)close(fd)

write系统调用触发内核led_write函数,通过copy_from_user获取用户数据,调用led_switch控制GPIO电平。


五、蜂鸣器驱动概述

5.1 蜂鸣器硬件电路

I.MX6U-ALPHA开发板上的蜂鸣器通过SNVS_TAMPER1引脚控制,复用为GPIO5_IO01。蜂鸣器驱动原理与LED类似,都是控制IO输出高低电平。

项目 说明
控制引脚 SNVS_TAMPER1 → GPIO5_IO01
有效电平 低电平鸣叫(0=开,1=关)
驱动方式 GPIO推挽输出
设备类型 有源蜂鸣器(电平驱动即可鸣叫)

[!note] 关于设备树极性 💡 蜂鸣器的实际有效电平为低电平:驱动内 gpio_set_value(dev->beep_gpio, 0) 直接输出低电平开启蜂鸣器、输出高电平关闭,并未使用 gpio_set_value_cansleep 或极性反转逻辑。

5.2 GPIO控制原理

蜂鸣器控制本质上是GPIO输出高低电平:

  • GPIO输出低电平(0)→ 蜂鸣器鸣叫
  • GPIO输出高电平(1)→ 蜂鸣器停止

蜂鸣器设备树配置需要:pinctrl信息、设备节点、GPIO属性。驱动程序使用of_find_node_by_pathof_get_named_gpio获取GPIO编号,通过gpio_set_value控制电平。


六、蜂鸣器设备树配置

6.1 pinctrl节点

imx6ull-alientek-emmc.dtsiomuxc节点imx6ul-evk子节点下添加:

pinctrl_beep: beepgrp {
    fsl,pins = <
        MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 0x10B0 /* beep */
    >;
};
属性 说明
MX6ULL_PAD_SNVS_TAMPER1__GPIO5_IO01 宏定义,将SNVS_TAMPER1复用为GPIO5_IO01
0x10B0 IO属性值

宏定义位于arch/arm/boot/dts/imx6ull-pinfunc-snvs.h

6.2 设备节点

在根节点"/"下创建蜂鸣器设备节点:

beep {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "atkalpha-beep";
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_beep>;
    beep-gpio = <&gpio5 1 GPIO_ACTIVE_HIGH>;
    status = "okay";
};
属性 说明
compatible 匹配驱动字符串"atkalpha-beep"
pinctrl-0 引用beepgrp pinctrl配置
beep-gpio 蜂鸣器使用的GPIO,gpio5的第1个引脚

6.3 引脚冲突检查

配置蜂鸣器前必须检查:

  1. SNVS_TAMPER1引脚是否被其他pinctrl节点使用,有则屏蔽
  2. GPIO5_IO01是否被其他外设使用,有则屏蔽

验证方法:编译设备树后启动系统,查看/proc/device-tree/beep节点是否存在。


七、蜂鸣器驱动源码分析

7.1 完整源码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define BEEP_CNT         1
#define BEEP_NAME        "beep"
#define BEEPOFF           0
#define BEEPON            1

struct beep_dev{
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    struct device_node  *nd;
    int beep_gpio;
};

struct beep_dev beep;

static int beep_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &beep;
    return 0;
}

static ssize_t beep_write(struct file *filp, const char __user *buf,
                          size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char beepstat;
    struct beep_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 == BEEPON) {
        gpio_set_value(dev->beep_gpio, 0);  /* 打开蜂鸣器 */
    } else if(beepstat == BEEPOFF) {
        gpio_set_value(dev->beep_gpio, 1);  /* 关闭蜂鸣器 */
    }
    return 0;
}

static int beep_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static struct file_operations beep_fops = {
    .owner = THIS_MODULE,
    .open = beep_open,
    .write = beep_write,
    .release = beep_release,
};

static int __init beep_init(void)
{
    int ret = 0;

    /* 1、获取设备节点 */
    beep.nd = of_find_node_by_path("/beep");
    if(beep.nd == NULL) {
        printk("beep node not find!\r\n");
        return -EINVAL;
    } else {
        printk("beep node find!\r\n");
    }

    /* 2、获取GPIO属性,得到GPIO编号 */
    beep.beep_gpio = of_get_named_gpio(beep.nd, "beep-gpio", 0);
    if(beep.beep_gpio < 0) {
        printk("can't get beep-gpio");
        return -EINVAL;
    }
    printk("beep-gpio num = %d\r\n", beep.beep_gpio);

    /* 3、设置GPIO为输出,高电平默认关闭 */
    ret = gpio_direction_output(beep.beep_gpio, 1);
    if(ret < 0) {
        printk("can't set gpio!\r\n");
    }

    /* 注册字符设备驱动 */
    /* 1、创建设备号 */
    if (beep.major) {
        beep.devid = MKDEV(beep.major, 0);
        register_chrdev_region(beep.devid, BEEP_CNT, BEEP_NAME);
    } else {
        alloc_chrdev_region(&beep.devid, 0, BEEP_CNT, BEEP_NAME);
        beep.major = MAJOR(beep.devid);
        beep.minor = MINOR(beep.devid);
    }
    printk("beep major=%d,minor=%d\r\n", beep.major, beep.minor);

    /* 2、初始化cdev */
    beep.cdev.owner = THIS_MODULE;
    cdev_init(&beep.cdev, &beep_fops);

    /* 3、添加cdev */
    cdev_add(&beep.cdev, beep.devid, BEEP_CNT);

    /* 4、创建类 */
    beep.class = class_create(THIS_MODULE, BEEP_NAME);
    if (IS_ERR(beep.class)) {
        return PTR_ERR(beep.class);
    }

    /* 5、创建设备 */
    beep.device = device_create(beep.class, NULL, beep.devid, NULL, BEEP_NAME);
    if (IS_ERR(beep.device)) {
        return PTR_ERR(beep.device);
    }

    return 0;
}

static void __exit beep_exit(void)
{
    cdev_del(&beep.cdev);
    unregister_chrdev_region(beep.devid, BEEP_CNT);
    device_destroy(beep.class, beep.devid);
    class_destroy(beep.class);
}

module_init(beep_init);
module_exit(beep_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

7.2 beep_init入口函数分析

蜂鸣器驱动初始化流程:

  1. 获取设备节点(120行):of_find_node_by_path("/beep")通过路径查找设备树节点
  2. 获取GPIO编号(129行):of_get_named_gpio(beep.nd, "beep-gpio", 0)从设备树属性中获取GPIO编号
  3. 设置GPIO方向(137行):gpio_direction_output(beep.beep_gpio, 1)设为输出,默认高电平关闭蜂鸣器
  4. 动态分配设备号(148行):alloc_chrdev_region自动分配主次设备号
  5. 初始化cdev(156行):cdev_init关联cdev与file_operations
  6. 创建类和设备(162~171行):class_createdevice_create自动在/dev下创建设备节点

7.3 beep_write函数分析

static ssize_t beep_write(struct file *filp, const char __user *buf,
                          size_t cnt, loff_t *offt)
{
    struct beep_dev *dev = filp->private_data;
    // ...
    if(beepstat == BEEPON) {
        gpio_set_value(dev->beep_gpio, 0);  // 低电平=蜂鸣器响
    } else if(beepstat == BEEPOFF) {
        gpio_set_value(dev->beep_gpio, 1);  // 高电平=蜂鸣器停
    }
}

通过filp->private_data获取设备结构体指针,使用GPIO子系统API gpio_set_value控制电平。

7.4 与LED驱动的关键差异

对比项 LED驱动 蜂鸣器驱动
设备节点获取 硬编码路径 of_find_node_by_path
GPIO获取方式 直接寄存器操作 of_get_named_gpio + gpio_set_value
设备号分配 register_chrdev(静态) alloc_chrdev_region(动态)
设备节点创建 手动mknod class_create + device_create自动创建
private_data 未使用 open时设置,write时使用

7.5 beepApp.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 BEEPOFF   0
#define BEEPON    1

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];

    /* 打开beep驱动 */
    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/beep文件写入数据 */
    retvalue = write(fd, databuf, sizeof(databuf));
    if(retvalue < 0){
        printf("BEEP 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;
}

八、实验验证

8.1 编译驱动程序

Makefile核心内容:

KERNELDIR := /home/zuozhongkai/linux/IMX6ULL/linux/temp/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := led.o    # LED驱动改为beep.o
build: kernel_modules
kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

编译命令:

make -j32           # 编译驱动模块
arm-linux-gnueabihf-gcc ledApp.c -o ledApp    # 编译测试APP

8.2 测试步骤

LED驱动测试

# 关闭心跳灯(出厂系统需要)
echo none > /sys/class/leds/sys-led/trigger

# 加载驱动
depmod              # 首次加载需要
modprobe led.ko

# 创建设备节点(非设备树方式)
mknod /dev/led c 200 0

# 测试
./ledApp /dev/led 1    # 点亮LED
./ledApp /dev/led 0    # 熄灭LED

# 卸载驱动
rmmod led.ko

蜂鸣器驱动测试

# 加载驱动
depmod
modprobe beep.ko

# 测试
./beepApp /dev/beep 1    # 蜂鸣器鸣叫
./beepApp /dev/beep 0    # 蜂鸣器停止

# 卸载驱动
rmmod beep.ko

8.3 效果验证

  • LED测试:执行./ledApp /dev/led 1后红色LED点亮,执行./ledApp /dev/led 0后熄灭
  • 蜂鸣器测试:执行./beepApp /dev/beep 1后蜂鸣器鸣叫,执行./beepApp /dev/beep 0后停止
  • 驱动加载成功后终端输出设备号信息(beep major=xxx,minor=xxx)
  • 原书实测:加载 beep.ko 时终端打印 beep node find!GPIO5_IO01 对应的 GPIO 编号为 129
  • 通过/proc/device-tree可验证设备树节点(gpioled/beep)是否正确加载

九、跨平台对比

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。

特性 I.MX6ULL (Linux) STM32 (裸机/HAL) RK3568 (Linux)
GPIO API gpio_set_value HAL_GPIO_WritePin gpio_set_value
设备树 支持 不支持 支持
时钟使能 设备树+驱动配置 __HAL_RCC_GPIOx_CLK_ENABLE 设备树+驱动配置
引脚复用 pinctrl子系统 GPIO_InitTypeDef pinctrl子系统
并发安全 内核保证 需手动处理 内核保证
设备节点 自动/手动创建 无(直接操作寄存器) 自动创建
调试方式 printk/dmesg 串口打印/LED闪烁 printk/dmesg
中断支持 request_irq HAL_NVIC_EnableIRQ request_irq

核心差异总结

  • Linux平台:驱动通过设备树获取硬件参数,使用内核提供的GPIO/pinctrl子系统API,驱动以模块形式加载卸载
  • STM32裸机:直接操作寄存器或使用HAL库,硬件参数在代码中定义,编译后固化在固件中
  • RK3568 Linux:与I.MX6ULL类似使用设备树,但引脚复用宏和寄存器地址不同

十、面试精选

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。

题目1:ioremap的作用是什么?为什么不能直接操作物理地址?

考察点:MMU与地址映射

参考答案: Linux开启MMU后,CPU访问的都是虚拟地址,直接操作物理地址会导致内核崩溃。ioremap将外设寄存器的物理地址映射到内核虚拟地址空间,驱动通过映射后的虚拟地址访问寄存器。iounmap在驱动卸载时释放映射,避免虚拟地址空间泄漏。对于I.MX6ULL,寄存器物理地址如0X020E0068必须先映射为虚拟地址才能操作。

题目2:misc设备和普通字符设备有什么区别?各自适用场景?

考察点:设备类型选择

参考答案

  • misc设备:次设备号固定为10,内核自动分配设备号和创建设备节点,适合简单外设驱动。使用misc_register注册,代码量少。
  • 普通字符设备:需手动分配设备号(alloc_chrdev_region)、初始化cdev、创建类和设备节点,适合复杂设备或多设备驱动。
  • 选择原则:简单GPIO控制(如蜂鸣器、LED)可用misc设备;需要多设备号、复杂文件操作的场景用普通字符设备。

题目3:设备树中compatible属性的作用?驱动如何匹配设备节点?

考察点:设备树匹配机制

参考答案compatible属性是设备树匹配驱动的核心字符串。驱动中通过of_match_table定义支持的compatible字符串列表,内核在加载驱动时遍历设备树节点的compatible属性,找到匹配项后调用驱动的probe函数。匹配过程由内核总线层完成。例如蜂鸣器驱动的compatible为"atkalpha-beep",设备树中节点的compatible也必须是"atkalpha-beep"才能匹配成功。

题目4:GPIO子系统中gpio_set_value和直接操作寄存器的区别?

考察点:GPIO子系统优势

参考答案

  • gpio_set_value:通过GPIO子系统API操作,内核自动处理寄存器偏移、时钟使能、并发保护,代码可移植性强,不同平台只需修改设备树
  • 直接操作寄存器:通过ioremap映射后用writel/readl操作,代码与硬件强耦合,移植性差,但执行效率略高
  • 推荐方式:Linux驱动应优先使用GPIO子系统API,这是内核推荐的标准做法,便于跨平台移植和维护

题目5:驱动中copy_from_user的作用?能否直接访问用户空间指针?

考察点:内核与用户空间数据交互

参考答案copy_from_user将用户空间数据安全复制到内核空间。不能直接访问用户空间指针,原因:①用户空间地址可能无效或已被交换到swap,直接访问会导致内核page fault崩溃;②用户空间地址可能没有内核要求的访问权限;③直接访问无法处理用户空间地址重映射的情况。copy_from_user内部会进行地址有效性检查和异常处理,确保内核安全。其返回值为未复制的字节数,0表示成功。


代码来源:正点原子I.MX6U嵌入式Linux驱动开发指南 V2.0.1(第四十一章、第四十六章) 内容来源: 《I.MX6U嵌入式Linux驱动开发指南》第41章 嵌入式Linux LED驱动开发实验、第46章 Linux蜂鸣器实验 最后更新: 2026-09-17