【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 驱动设备程序。

相关推荐
爱和冰阔落16 分钟前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨18 分钟前
WSL2的安装与使用
linux·wsl
微小的鱼29 分钟前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng1 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮2 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky13 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默13 小时前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu
skywalk816314 小时前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd
爱莉希雅&&&16 小时前
GitLab 完整学习笔记
linux·运维·服务器
风笙97816 小时前
[文件系统]文件系统挂载与卸载详解
linux