1. 搭建嵌入式Linux BSP开发环境

任务说明

本案例实现一个最简字符设备驱动,通过操作GPIO寄存器点亮开发板上的LED。驱动加载时自动初始化GPIO并点亮LED,卸载时关闭LED并释放资源。同时提供设备文件操作接口,支持用户程序通过write()控制LED亮灭,以此掌握Linux驱动模块的加载、卸载与基本字符设备框架。

硬件前提

以i.MX6ULL开发板为例,LED接在GPIO1_IO03引脚(低电平点亮)。寄存器基地址:

  • CCM_CCGR1: 0x020C406C(使能GPIO1时钟)
  • IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03: 0x020E0068(复用为GPIO功能)
  • IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03: 0x020E02F4(电气属性)
  • GPIO1_GDIR: 0x0209C004(方向寄存器,bit3=1输出)
  • GPIO1_DR: 0x0209C000(数据寄存器,bit3=0点亮)

驱动代码实现

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/slab.h>

#define LED_MAJOR 0
#define LED_NAME "led_drv"

/* 寄存器物理地址 */
#define CCM_CCGR1_ADDR       0x020C406C
#define IOMUX_MUX_ADDR       0x020E0068
#define IOMUX_PAD_ADDR       0x020E02F4
#define GPIO1_GDIR_ADDR      0x0209C004
#define GPIO1_DR_ADDR        0x0209C000

static void __iomem *ccm_ccgr1;
static void __iomem *iomux_mux;
static void __iomem *iomux_pad;
static void __iomem *gpio1_gdir;
static void __iomem *gpio1_dr;

static dev_t led_devno;
static struct cdev led_cdev;
static struct class *led_class;
static struct device *led_device;

/* 打开设备 */
static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 写操作:buf[0]=1点亮,buf[0]=0熄灭 */
static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t count, loff_t *ppos)
{
    char val;
    int ret;

    if (count < 1)
        return -EINVAL;

    ret = copy_from_user(&val, buf, 1);
    if (ret)
        return -EFAULT;

    if (val == '1')
        writel(readl(gpio1_dr) & ~(1 << 3), gpio1_dr);  /* 低电平点亮 */
    else if (val == '0')
        writel(readl(gpio1_dr) | (1 << 3), gpio1_dr);   /* 高电平熄灭 */
    else
        return -EINVAL;

    return count;
}

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

/* 初始化LED相关GPIO */
static void led_hw_init(void)
{
    u32 val;

    /* 1. 使能GPIO1时钟 */
    ccm_ccgr1 = ioremap(CCM_CCGR1_ADDR, 4);
    val = readl(ccm_ccgr1);
    val |= (3 << 26);   /* bit26-27: GPIO1时钟 */
    writel(val, ccm_ccgr1);

    /* 2. 引脚复用为GPIO */
    iomux_mux = ioremap(IOMUX_MUX_ADDR, 4);
    val = readl(iomux_mux);
    val &= ~0xF;
    val |= 0x5;          /* ALT5 = GPIO1_IO03 */
    writel(val, iomux_mux);

    /* 3. 电气属性:上拉,200K,SRE=0 */
    iomux_pad = ioremap(IOMUX_PAD_ADDR, 4);
    writel(0x10B0, iomux_pad);

    /* 4. 方向:输出 */
    gpio1_gdir = ioremap(GPIO1_GDIR_ADDR, 4);
    val = readl(gpio1_gdir);
    val |= (1 << 3);
    writel(val, gpio1_gdir);

    /* 5. 默认点亮LED */
    gpio1_dr = ioremap(GPIO1_DR_ADDR, 4);
    val = readl(gpio1_dr);
    val &= ~(1 << 3);
    writel(val, gpio1_dr);
}

static int __init led_init(void)
{
    int ret;

    /* 1. 注册字符设备区域 */
    ret = alloc_chrdev_region(&led_devno, 0, 1, LED_NAME);
    if (ret < 0)
        return ret;

    /* 2. 初始化cdev并添加 */
    cdev_init(&led_cdev, &led_fops);
    led_cdev.owner = THIS_MODULE;
    ret = cdev_add(&led_cdev, led_devno, 1);
    if (ret)
        goto err_cdev;

    /* 3. 创建设备类 */
    led_class = class_create(THIS_MODULE, LED_NAME);
    if (IS_ERR(led_class)) {
        ret = PTR_ERR(led_class);
        goto err_class;
    }

    /* 4. 创建设备节点 /dev/led_drv */
    led_device = device_create(led_class, NULL, led_devno, NULL, LED_NAME);
    if (IS_ERR(led_device)) {
        ret = PTR_ERR(led_device);
        goto err_device;
    }

    /* 5. 硬件初始化并点亮LED */
    led_hw_init();
    printk(KERN_INFO "led_drv: initialized, LED ON\n");

    return 0;

err_device:
    class_destroy(led_class);
err_class:
    cdev_del(&led_cdev);
err_cdev:
    unregister_chrdev_region(led_devno, 1);
    return ret;
}

static void __exit led_exit(void)
{
    u32 val;

    /* 关闭LED */
    val = readl(gpio1_dr);
    val |= (1 << 3);
    writel(val, gpio1_dr);

    /* 释放映射 */
    iounmap(ccm_ccgr1);
    iounmap(iomux_mux);
    iounmap(iomux_pad);
    iounmap(gpio1_gdir);
    iounmap(gpio1_dr);

    /* 注销设备 */
    device_destroy(led_class, led_devno);
    class_destroy(led_class);
    cdev_del(&led_cdev);
    unregister_chrdev_region(led_devno, 1);

    printk(KERN_INFO "led_drv: unloaded, LED OFF\n");
}

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("BSP Course");
MODULE_DESCRIPTION("First char device driver: GPIO LED control");

Makefile

obj-m := led_drv.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

编译与加载

# 在开发板或同架构主机上执行
make
sudo insmod led_drv.ko
ls /dev/led_drv
dmesg | tail -5

预期输出(dmesg):led_drv: initialized, LED ON,同时开发板上LED点亮。

用户程序测试

/* test_led.c */
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    int fd;
    char cmd;

    if (argc != 2) {
        printf("Usage: %s [0|1]\n", argv[0]);
        return -1;
    }

    fd = open("/dev/led_drv", O_WRONLY);
    if (fd < 0) {
        perror("open");
        return -1;
    }

    cmd = argv[1][0];
    if (write(fd, &cmd, 1) != 1)
        perror("write");

    close(fd);
    return 0;
}
gcc test_led.c -o test_led
./test_led 1   # LED点亮
./test_led 0   # LED熄灭

卸载驱动

sudo rmmod led_drv
dmesg | tail -3

预期输出:led_drv: unloaded, LED OFF,LED熄灭。

验证判定标准

  • insmod后dmesg出现初始化日志,LED点亮,/dev/led_drv节点存在;
  • 执行./test_led 1LED点亮,./test_led 0LED熄灭;
  • rmmod后dmesg出现卸载日志,LED熄灭,设备节点消失。
LED字符设备驱动架构与调用时序 用户空间 test_led 程序 open("/dev/led_drv") write(fd, "1"/"0", 1) close(fd) 内核空间 VFS 虚拟文件系统 led_fops (open/write) ioremap + readl/writel GPIO1_DR bit3 控制 硬件层 CCM_CCGR1 (时钟) IOMUXC (引脚复用) GPIO1_DR/GDIR 系统调用 寄存器读写 LED GPIO1_IO03 insmod → module_init → led_hw_init() → LED点亮 rmmod → module_exit → LED熄灭 验证路径:insmod → dmesg确认 → /dev/led_drv → test_led 1/0 → rmmod 判定标准:LED亮灭与write参数一致,卸载后LED熄灭且节点消失