【Linux驱动开发】字符设备介绍 | LED灯驱动基本实现(自动申请设备号+手动添加节点)

文章目录

    • 前言
    • 一、字符设备驱动框架
      • [1.1 字符设备驱动框架图解](#1.1 字符设备驱动框架图解)
      • [1.2 字符设备驱动编写的步骤](#1.2 字符设备驱动编写的步骤)
      • [1.3 相关函数接口](#1.3 相关函数接口)
    • 二、编写LED灯驱动流程
      • [2.1 原理图](#2.1 原理图)
      • [2.2 相关寄存器](#2.2 相关寄存器)
      • [2.3 内存映射函数接口](#2.3 内存映射函数接口)
      • [2.4 寄存器的读写接口](#2.4 寄存器的读写接口)
    • 三、逐步编写LED灯驱动
      • [3.1 构建驱动工程框架](#3.1 构建驱动工程框架)
      • [3.2 申请注册设备号](#3.2 申请注册设备号)
      • [3.3 初始化字符设备结构体](#3.3 初始化字符设备结构体)
      • [3.4 LED寄存器配置](#3.4 LED寄存器配置)
      • [3.5 LED操作函数完善](#3.5 LED操作函数完善)
      • [3.6 应用层主函数](#3.6 应用层主函数)
      • [3.7 交互界面操作](#3.7 交互界面操作)

前言

在上篇文章【Linux驱动开发】驱动介绍及驱动工程搭建中,我们学习了Linux驱动的分类,还完成了对驱动工程构建的具体了解与具体实现,在这篇文章中我们将实现字符设备LED的驱动编写,从51的点灯大师到IMX6ULL裸机的点灯大师,到现在即将成为Linux驱动的点灯大师了。

一、字符设备驱动框架

1.1 字符设备驱动框架图解

从上面的图中可以看出,其实都在围绕三个数据结构转:

  1. cdev 结构体
    字符设备内核对象,内核里代表一个字符设备实例。保存设备号dev_t和操作方法集合file_operations;通过cdev_add()向内核注册、cdev_del()注销,是字符设备驱动的核心枢纽。
  2. struct file_operations 文件操作方法表
    函数指针集合,存放open/read/write/release/ioctl等回调函数。应用层open/read/write系统调用,经过 VFS(虚拟文件系统) 最终会调用这里注册的驱动函数;驱动只需要实现自己用到的接口即可,不需要全部填充。
  3. dev_t 设备号类型(不是结构体,是数据类型)
    存放主设备号 + 次设备号。

理解这三个数据结构,字符设备驱动编写就更容易理解了。

1.2 字符设备驱动编写的步骤

  1. 申请设备号,主次设备号 MKDEV, alloc_chrdev_region
  2. 注册设备号 register_chrdev_region , alloc_chrdev_region
  3. 初始化字符设备结构体 cdev_init(自动关联fops), cdev_add(驱动注册到内核),cdev_alloc (手动关联)
  4. 准备 file_operation , 注册 应用层对应的open,close,read, write函数。给函数指针赋值

1.3 相关函数接口

  1. cdev操作函数接口

    函数名 功能
    cdev_init 初始化字符设备结构体
    cdev_alloc 申请字符设备结构体
    cdev_add 添加字符设备
    cdev_del 删除字符设备
  2. 字符设备操作接口

    函数名 功能
    register_chrdev_region 静态注册已知设备号范围(需提前确定主设备号)
    alloc_chrdev_region 动态分配设备号,并注册(内核自动分配主设备号)
    unregister_chrdev_region 释放注册的设备号
    register_chrdev 注册设备号并关联file_operations结构体(早期内核使用,2.6版本以后尽量不使用)
    unregister_chrdev 注销注册的设备号(早期内核使用,2.6版本以后尽量不使用)
    宏 作用 输入 输出
    MKDEV(ma, mi) 将主设备号、次设备号合并生成dev_t设备号 ma:主设备号;mi:次设备号 dev_t 组合设备号
    MAJOR(dev) 从dev_t中提取主设备号 dev_t类型设备号 int 主设备号
    MINOR(dev) 从dev_t中提取次设备号 dev_t类型设备号 int 次设备号

    当创建设备类型后,进入/proc/devices目录下会发现没有具体的设备 。需要我们手动创建节点:

    mknod /dev/led0 c 248 0

  3. file_operation 结构体

二、编写LED灯驱动流程

2.1 原理图

根据字符设备驱动流程,编写LED灯驱动,实现应用程序控制LED灯亮灭。

在IMX6ULL这块开发板上LED的原理图如下:

低电平亮,高电平灭。引脚为GPIO1_03。

2.2 相关寄存器

在裸机阶段,我们已经实现过LED灯的相关操作,其主要涉及到的寄存器如下:

  1. IO引脚复用寄存器

  2. IO引脚电气特性

  3. GPIO 方向

  4. gpio 的数据寄存器

  5. 时钟寄存器 ,不需要设置,裸机需要设置

Linux 内核不能直接操作物理地址 !内核开启 MMU,使用虚拟地址。

需要调用 ioremap(),把上面的物理地址映射成内核虚拟地址,驱动代码操作映射后的虚拟地址。因此我们还需要找到这些寄存器的物理地址:

寄存器 名称 地址
IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 引脚复用功能 0x20E0068
IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO03 电气特性 0x20E02F4
GPIO1_DR 数据寄存器 0x209C000
GPIO1_GDIR 方向 0x209C004

2.3 内存映射函数接口

函数名 功能
ioremap 将内核虚拟地址映射到物理地址 //#include <linux/io.h> #include <asm/io.h>
iounmap 解除映射的虚拟地址
devm_ioremap 将物理地址映射到内存虚拟地址空间(驱动卸载时自动解除映射)
devm_iounmap 提前解除映射关系
devm_ioremap_resource 整合资源申请并映射,避免硬件冲突,适用设备树场景
copy_to_user 内核层向用户层拷贝数据
copy_from_user 从用户层拷贝数据到内核层

2.4 寄存器的读写接口

函数名 功能
readb(regaddr) 读寄存器(regaddr地址)1个字节 u8
readw(regaddr) 读寄存器2个字节 u16
readl(regaddr) 读寄存器4个字节 u32
writeb(v,c) 写寄存器1个字节,(value,regaddr)
writew(v,c) 写寄存器2个字节,(value,regaddr)
writel(v,c) 写寄存器4个字节,(value,regaddr)

三、逐步编写LED灯驱动

3.1 构建驱动工程框架

将上篇文章中构建的驱动工程框架可以搬过来,记得修改相关的名字。

3.2 申请注册设备号

设备号的获得有两种方式:

  1. 静态注册已知设备号范围(需提前确定主设备号)

    MKDEV+register_chrdev_region

  2. 动态分配设备号,并注册(内核自动分配主设备号)

    alloc_chrdev_region

    在本篇文章中就采用第二中,使得编写更容易。

    若使用alloc_chrdev_region时,vscode没有自动补齐添加头文件时,可以使用vi -t '函数名'来查询函数接口所在的文件。

    参数 说明
    dev_t *dev 输出参数 。函数成功后,将拼接好的dev_t(主+次设备号)写入该地址。 外部使用 MAJOR(*dev) 获取主设备号,MINOR(*dev) 获取起始次设备号。
    unsigned baseminor 起始次设备号,申请次设备号区间的第一个编号,一般填 0。
    unsigned count 需要连续占用的次设备号数量。 例:baseminor=0, count=4 → 占用次设备号 0,1,2,3。
    const char *name 设备名称,会显示在 /proc/devices,用于查看设备号。

    在了解函数后,我们就可以这样写:

3.3 初始化字符设备结构体

  1. 初始化字符设备结构体 cdev_init(自动关联fops), cdev_add(驱动注册到内核)
    这里已经需要fops与cdev结构体了,我们先将其定义出来:

    c 复制代码
    //设备结构体
    static struct cdev cdev;
    
    //对自己的操作函数先进行隐式声明,使其与fops关联
    static int myopen(struct inode *node, struct file *f);
    static int myclose(struct inode *node, struct file *f);
    static ssize_t myread(struct file *f, char __user *user, size_t size, loff_t *loff);
    static ssize_t mywrite(struct file *f, const char __user *user, size_t size, loff_t *loff);
    
    //定义文件操作结构体,选取自己需要的操作
    static struct file_operations fops = {
        .owner = THIS_MODULE,
        .open = myopen,
        .release = myclose,
        .read = myread,
        .write = mywrite,
    };
    
    static int myopen(struct inode *node, struct file *f)
    {
        return 0;
    }
    ......

    然后初始化字符设备结构体:

    c 复制代码
    //设备结构体初始化,关联fops文件操作结构体
    cdev_init(&cdev, &fops);
    //注册设备到内核
    ret = cdev_add(&cdev, dev_no, 1);
    if (ret < 0)
    {
       pr_err("cdev_add error\n");
       return ret;
    }

到此led这个设备在Linux中已经有了,接下来就是对LED灯的相关操作进行配置了。

3.4 LED寄存器配置

寄存器配置:

c 复制代码
io_mux = ioremap(IO_MUX, 4);
io_pad = ioremap(IO_PAD, 4);
io_data = ioremap(IO_DATA, 4);
io_dir = ioremap(IO_DIR, 4);

//设置GPIO复用寄存器
v = readl(io_mux);
v = v | 0x05;
writel(v, io_mux);

//设置GPIO电气寄存器
v = readl(io_pad);
v = v | 0x10b0;
writel(v, io_pad);

//设置GPIO数据
v = readl(io_data);
v = v | (1 << 3);
writel(v, io_data);

//设置输入输出方向
v = readl(io_dir);
v = v | (1 << 3);
writel(v, io_dir);

3.5 LED操作函数完善

c 复制代码
static ssize_t mywrite(struct file *f, const char __user *data, size_t size, loff_t *loff)
{
    unsigned long ret = 0;
    int led_state = 0;
    u32 v = 0;

    ret = copy_from_user(&led_state, data, 4);

    if (0 == led_state)
    {
        // led off
        v = readl(io_data);
        v = v | (1 << 3);
        writel(v, io_data);
    }
    else if (1 == led_state)
    {
        // led on
        v = readl(io_data);
        v = v & ~(1 << 3);
        writel(v, io_data);
    }

    return size;
}
c 复制代码
static int myclose(struct inode *node, struct file *f)
{
    iounmap(io_dir);
    iounmap(io_data);
    iounmap(io_pad);
    iounmap(io_mux);

    return 0;
}

3.6 应用层主函数

实现led灯1秒亮灭:

c 复制代码
int main(void)
{
    int fd = open("/dev/led0", O_RDWR);
    if (-1 == fd)
    {
        perror("open error");
        return -1;
    }

    int on = 0;

    while (1)
    {
        write(fd, &on, sizeof(on));
        sleep(1);
        if (0 == on)
        {
            on = 1;
        }
        else if (1 == on)
        {
            on = 0;
        }
    }

    return 0;
}

3.7 交互界面操作

此时,会发现led没有按照应用层写的代码一秒闪烁,原因是在设备树中有一个用来表示设备运行的函数占用了此LED,因此,我们需要在arch/arm/boot/dts/imx6ull-alientek-emmc.dts中, 注释dtsleds,然后重新编译dts。

这样再次重复操作后就会发现led开始正常工作了。

当然,目前的这个led驱动还不完整,比如说,若设中途出现错误,会直接返回,这样之前申请的一些空间就没有释放,会造成内存资源泄漏,然后还有手动创建节点很麻烦,有没有自动创建节点的方法,答案是当然有,这些都会在后面一篇主题为sys文件系统与misc的文章中提到。

相关推荐
gscsded1 天前
MCUboot裸机移植实战
单片机·嵌入式硬件·arm
想喝奶茶啾啾1 天前
AMBA CHI架构以及规范中使用的术语 翻译笔记(1)
笔记·arm·chi
susplus2 天前
【IMX6ULL Linux系统移植】内核编译裁剪 + rootfs 构建 + 完整启动流程梳理
linux内核·arm·rootfs·linux启动
G.E.M.小白3 天前
【Linux嵌入式蜂鸣器驱动开发】原理图分析、寄存器寻址、完整驱动+应用+Makefile
嵌入式·imx6ull·linux驱动开发·蜂鸣器驱动
susplus4 天前
【IMX6ULL Linux系统移植】SD卡分区 | uboot详解
linux·arm·u-boot
剑指offer.8 天前
ARM裸机开发-SPI
arm开发·嵌入式硬件·嵌入式·arm
susplus11 天前
【ARM 裸机开发 (IMX6ULL-mini)】I2C协议详解
嵌入式·arm·i2c
今年下半年12 天前
【运维】windows虚拟机安装arm(aarch64)服务器
运维·windows·arm·虚拟机·aarch64
额额额对了13 天前
SPI通信
linux·c语言·汇编·单片机·嵌入式硬件·arm