【Linux驱动篇】LED驱动开发实验

文章目录

  • 【Linux驱动篇】LED驱动开发实验
    • [1 简介](#1 简介)
      • [1.1 地址映射](#1.1 地址映射)
        • [1.1.1 ioremap函数](#1.1.1 ioremap函数)
        • [1.1.2 iounmap函数](#1.1.2 iounmap函数)
      • [1.2 IO内存访问函数](#1.2 IO内存访问函数)
        • [1.2.1 读操作函数](#1.2.1 读操作函数)
        • [1.2.2 写操作函数](#1.2.2 写操作函数)
    • [2 硬件原理分析](#2 硬件原理分析)
    • [3 实验程序编写](#3 实验程序编写)
      • [3.1 新建工程](#3.1 新建工程)
      • [3.1 LED灯驱动程序编写](#3.1 LED灯驱动程序编写)
      • [3.2 编写测试APP](#3.2 编写测试APP)
      • [3.3 编写Makefile](#3.3 编写Makefile)
    • [4 编译测试](#4 编译测试)
      • [4.1 编译](#4.1 编译)
        • [4.1.1 编译驱动程序](#4.1.1 编译驱动程序)
        • [4.1.2 编译测试APP](#4.1.2 编译测试APP)
      • [4.2 运行测试](#4.2 运行测试)

【Linux驱动篇】LED驱动开发实验

1 简介

Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器,而硬件寄存器又会在Linux中映射到内存中使用,因此我们操作内存即可实现配置硬件寄存器。

1.1 地址映射

MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。主要完成的功能如下:

  • 完成虚拟空间到物理空间的映射。
    • 虚拟地址:对于 32 位的处理器来说,虚拟地址范围是 2^32=4GB,
    • 物理地址:开发板上有 512MB 的 DDR3,这 512MB 的内存就是物理内存
  • 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

经过 MMU 可以将开发板的物理地址映射到整个 4GB 的虚拟空间,如下所示:

Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址。

举个例子:I.MX6ULL 的 GPIO1_IO03 引脚的复用寄存IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 的地址为 0X020E0068,如果没有开启MMU的话,可以直接完该寄存器里面写入数据即可配置GPIO1_IO03的复用功能,但是开启了MMU,设置了内存映射,就不能直接写入了,需要得到Linux系统内对应的虚拟地址,然后写入。这里涉及到两个函数:ioremap 和 iounmap

1.1.1 ioremap函数

用于获取指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间 , 定 义 在arch/arm/include/asm/io.h 文件中,定义如下:

c 复制代码
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
{
    return arch_ioremap_caller(phys_addr, size, mtype, __builtin_return_address(0));
}

其中:

  • ioremap 是个宏,有两个参数:cookie 和 size,真正起作用的是函数__arm_ioremap。
  • phys_addr:要映射的物理起始地址。
  • size:要映射的内存空间大小。
  • mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。
  • 返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。

假如我们要获取 I.MX6ULL 的 IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器对应的虚拟地址,使用如下:

sh 复制代码
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
static void __iomem* SW_MUX_GPIO1_IO03;
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);

其中:

  • 宏 SW_MUX_GPIO1_IO03_BASE 是寄存器物理地址
  • SW_MUX_GPIO1_IO03 是映射后的虚拟地址
  • 对于 I.MX6ULL 来说一个寄存器是 4 字节(32 位)的,因此映射的内存长度为 4

映射完成以后直接对 SW_MUX_GPIO1_IO03 进行读写操作即可

1.1.2 iounmap函数

卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射,iounmap 函数原型如下:

c 复制代码
void iounmap (volatile void __iomem *addr)

其中:

  • addr: 要取消映射的虚拟地址空间首地址。

假如我们想要取消IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 寄存器的地址映射,使用如下代码:

c 复制代码
iounmap(SW_MUX_GPIO1_IO03);

1.2 IO内存访问函数

当外部寄存器或内存映射到 IO 空间时,称为 I/O 端口。当外部寄存器或内存映射到内存空间时,称为 I/O 内存。

对于 ARM 来说没有 I/O 空间这个概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。

Linux提供了一组操作函数对映射后的内存进行读写操作。

1.2.1 读操作函数

有如下几个:

c 复制代码
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

其中:

  • readb、readw 和 readl 这三个函数分别对应 8bit、16bit 和 32bit 读操作
  • addr 就是要读取写内存地址
  • 返回值就是读取到的数据。
1.2.2 写操作函数

有如下几个:

c 复制代码
u8 writeb(u8 value, volatile void __iomem *addr)
u16 writew(u8 value,volatile void __iomem *addr)
u32 writel(u8 value,volatile void __iomem *addr)

其中:

  • writeb、writew 和 writel 这三个函数分别对应 8bit、16bit 和 32bit 写操作
  • value 是要写入的数值
  • addr 是要写入的地址。

2 硬件原理分析

I.MX6U-ALPHA开发板上有一个LED灯,原理图如下:

LED0 接到了 GPIO_3 上,GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。

3 实验程序编写

3.1 新建工程

新建工程,其目录结构如下所示:

sh 复制代码
pzs@pzs-jammy:~/linux/drivers$ tree ./2_led/ -a
./2_led/
├── ledApp.c
├── led.c
├── Makefile
└── .vscode
    └── c_cpp_properties.json

其中:

  • c_cpp_properties.json: 配置vscode的头文件引入。参考【Linux驱动篇】字符设备驱动开发
  • Makefile: 编译工程
  • led.c:驱动程序
  • ledApp.c: 驱动测试程序

3.1 LED灯驱动程序编写

在 led.c 里面输入如下内容:

c 复制代码
#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 /* major number */
#define LED_NAME "led" /* driver name */

#define LEDOFF 0 /* LED off */
#define LEDON 1 /* LED on */

/* Register physical address */
#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)

/* Pointer to the virtual address of the mapped register */
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;

/**
 * @brief: Switch the LED on/off
 * @param state: The state of the LED, LEDOFF or LEDON
 */
void led_switch(u8 state) {
    u32 val = 0;
    if(state == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);
        writel(val, GPIO1_DR);
    } else if(state == LEDOFF) {
        val = readl(GPIO1_DR);
        val |= (1 << 3);
        writel(val, GPIO1_DR);
    }
}

/**
 * @description: led_open - open function
 * @param - inode: inode of device file
 * @param - filp: device file
 * @return: 0 on success, -1 on failure
 */
static int led_open(struct inode *inode, struct file *filp)
{
    printk("led open!\r\n");
    return 0;
}

/**
 * @description: led_read - read function
 * @param - filp: device file
 * @param - buf: user buffer
 * @param - cnt: count of bytes to read
 * @param - offt: offset of file
 * @return: count of bytes read
 */
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    printk("led read!\r\n");
    return 0;
}

/**
 * @description: led_write - write function
 * @param - filp: device file
 * @param - buf: user buffer
 * @param - cnt: count of bytes to write
 * @param - offt: offset of file
 * @return: count of bytes written
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue = 0;
    unsigned char databuf[1];
    unsigned char ledstat;

    printk("led write!\r\n");
    /* copy user data to kernel buffer */
    retvalue = copy_from_user(databuf, buf, cnt);
    if(retvalue < 0){
        printk("kernel write failed!\r\n");
        return -EFAULT;
    } 

    /* get led status */
    ledstat = databuf[0];

    /* switch led */
    led_switch(ledstat);

    return 0;
}

/**
 * @description: led_release - release function
 * @param - inode: inode of device file
 * @param - filp: device file
 * @return: 0 on success, -1 on failure
 */
static int led_release(struct inode *inode, struct file *filp)
{
    printk("led release!\r\n");
    return 0;
}

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

/**
 * @description: led_init - initialization function
 * @return: 0 on success, -1 on failure
 */
static int __init led_init(void)
{
    int retvalue = 0;
    u32 val = 0;

    /* Init LED */
    /* Register address mapping */
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 0x4);
    SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 0x4);
    SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 0x4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 0x4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 0x4);

    /* Enable GPIO1 clock */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26); /* Clear bits 26 and 27 */
    val |= (3 << 26); /* Set bits 26 and 27 */
    writel(val, IMX6U_CCM_CCGR1);

    /* Set GPIO1_IO03 as GPIO output */
    writel(5, SW_MUX_GPIO1_IO03);

    /* Set GPIO1_IO03 as GPIO push-pull output */
    writel(0x10B0, SW_PAD_GPIO1_IO03);

    /* Set GPIO1_IO03 as output */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);
    val |= (1 << 3);
    writel(val, GPIO1_GDIR);

    /* Switch LED off */
    led_switch(LEDOFF);

    retvalue = register_chrdev(LED_MAJOR, LED_NAME, 
        &led_fops);
    if(retvalue < 0)
    {
        printk("led register failed!\r\n");
        return -EIO;
    }

    printk("led register success!\r\n");
    return 0;
}

/**
 * @description: led_exit - exit function
 * @brief: unregister chrdev
 */
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);
    printk("led unregister success!\r\n");
}

/* module init and exit */
module_init(led_init);
module_exit(led_exit);

/* module information */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("pzs");

3.2 编写测试APP

功能:编写测试 APP,led 驱动加载成功以后手动创建/dev/led 节点,应用 APP 通过操作/dev/led文件来完成对 LED 设备的控制。向/dev/led 文件写 0 表示关闭 LED 灯,写 1 表示打开 LED 灯。

实现:在ledApp.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

/**
 * @brief: Main function
 * @param argc: The number of command line arguments
 * @param argv: An array of command line arguments
 * @return: 0 on success, -1 on failure
 */
int main(int argc, char *argv[]) {
    int fd, retvalue;
    char *filename;
    unsigned char databuf[1];
    if(argc != 3) {
        printf("usage: %s <device_path> <operation>\n", argv[0]);
        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if(fd < 0) {
        printf("open %s failed!\n", filename);
        return -1;
    }

    databuf[0] = atoi(argv[2]);

    retvalue = write(fd, databuf, sizeof(databuf));
    if(retvalue < 0) {
        printf("write %s failed!\n", filename);
        close(fd);
        return -1;
    }

    /* close device file */
    retvalue = close(fd);
    if(retvalue < 0) {
        printf("close %s failed!\n", filename);
        return -1;
    }

    return 0;
}

3.3 编写Makefile

编写Makefile用于编译驱动程序:

makefile 复制代码
KERNELDIR := /home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga

CURRENT_PATH := $(shell pwd)

ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-

obj-m := led.o

build: kernel_modules

kernel_modules:
	$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(CURRENT_PATH)  modules

clean:
	$(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

4 编译测试

4.1 编译

4.1.1 编译驱动程序

输入如下命令编译出驱动模块文件:

sh 复制代码
pzs@pzs-jammy:~/linux/drivers/2_led$ make
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C /home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga M=/home/pzs/linux/drivers/2_led  modules
make[1]: Entering directory '/home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'
  CC [M]  /home/pzs/linux/drivers/2_led/led.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/pzs/linux/drivers/2_led/led.mod.o
  LD [M]  /home/pzs/linux/drivers/2_led/led.ko
make[1]: Leaving directory '/home/pzs/linux/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga'

编译成功以后就会生成一个名为"led.ko"的驱动模块文件。

4.1.2 编译测试APP

输入如下命令编译测试 ledApp.c 测试程序:

SH 复制代码
pzs@pzs-jammy:~/linux/drivers/2_led$ arm-linux-gnueabihf-gcc ledApp.c -o ledApp

编译成功以后就会生成 ledApp 这个应用程序。

4.2 运行测试

参考【Linux驱动篇】字符设备驱动开发从NFS启动根文件系统,然后将编译出来的led.ko和ledApp这两个文件拷贝到开发板的rootfs/lib/modules/4.1.15目录中:

sh 复制代码
pzs@pzs-jammy:~/linux/drivers/2_led$ sudo cp ./led.ko ./ledApp ~/linux/nfs/ubuntu_rootfs/lib/modules/4.1.15/

在开发板,输入如下命令加载 led.ko 驱动模块:

sh 复制代码
root@alientek_imx6ul:/home/pzs# cd /lib/modules/4.1.15/
root@alientek_imx6ul:/lib/modules/4.1.15# modprobe led

驱动加载成功以后创建"/dev/led"设备节点,命令如下:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# mknod /dev/led c 200 0

驱动节点创建成功以后就可以使用 ledApp 软件来测试驱动是否工作正常,输入如下命令打开 LED 灯:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# ./ledApp /dev/led 1

输入如下命令关闭 LED 灯:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# ./ledApp /dev/led 0

如果要卸载驱动的话输入如下命令即可:

sh 复制代码
root@alientek_imx6ul:/lib/modules/4.1.15# rmmod led.ko

至此,我们成功编写了第一个真正的 Linux 驱动设备程序。

相关推荐
李斯维2 小时前
安装 WSL 最好的方式
linux·windows
张小九992 小时前
fpocket安装和使用教程
linux·机器学习·github
元气满满-樱2 小时前
Rewrite重写
linux·nginx
杰克崔2 小时前
进程内mmap锁相互干扰问题
linux·运维·服务器·车载系统
xlp666hub3 小时前
从零手写一个 printf 函数:变参宏与默认参数提升
linux
迅为电子3 小时前
迅为iTOP-Hi3516开发板linux驱动开发资料全面上线,构建从入门到精通的完整学习路径!
linux·驱动开发·学习
代码游侠3 小时前
应用——Linux进程通信与信号处理
linux·运维·服务器·笔记·学习·信号处理
HalvmånEver3 小时前
Linux:Ext系列⽂件系统(二)
linux·运维·服务器
石像鬼₧魂石3 小时前
内网渗透靶场 攻击 & 排错命令分类速查表
linux·windows·学习·ubuntu