Linux 驱动开发 五:Linux LED驱动开发

Linux 下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED 灯驱动最终也是对 I.MX6ULLIO 口进行配置,与裸机实验不同的是,在 Linux 下编写驱动要符合 Linux 的驱动框架。

一、地址映射

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

在编写驱动之前,我们需要先简单了解一下 MMU 这个神器,MMU 全称叫做 Memory Manage Unit,也就是内存管理单元。在老版本的 Linux 中要求处理器必须有 MMU,但是现在 Linux 内核已经支持无MMU的处理器了。MMU主要完成的功能如下:

1、完成虚拟空间到物理空间的映射。

2、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。

物理内存和虚拟内存之间的转换,需要用到两个函数:ioremapiounmap

1、ioremap

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

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

2、iounmap

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

void iounmap (volatile void __iomem *addr)

二、I/O内存访问函数

这里说的 I/O输入/输出的意思。这里涉及到两个概念I/O 端口I/O 内存

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

外部寄存器内存映射到内存空间时,称为 I/O 内存

但是对于 ARM 来说没有 I/O 空间这个概念,因此 ARM 体系下只有 I/O 内存(可以直接理解为内存)。使用 ioremap 函数将寄存器的物 理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。

1、读操作函数

u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)

readbreadwreadl 这三个函数分别对应 8bit16bit32bit 读操作,参数 addr 就是要读取写内存地址,返回值就是读取到的数据。

2、写操作函数

void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)

writebwritewwritel 这三个函数分别对应 8bit16bit32bit 写操作,参数 value 是要写入的数值,addr 是要写入的地址。

三、硬件原理图分析

https://blog.csdn.net/OnlyLove_/article/details/121757151

四、实验程序编写

1、工程配置

创建工程文件 c_cpp_properties.json,并添加头文件路径。

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
                "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
                "/home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu17",
            "cppStandard": "gnu++14",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

2、搭建驱动框架

1、led.c

#include "linux/init.h"
#include "linux/module.h"
#include "linux/fs.h"
#include "linux/types.h"

// struct inode 声明在 linux/fs.h 中
// struct file 声明在 linux/fs.h 中
int led_open (struct inode *i, struct file *f)
{
    // printk 声明在 linux/printk.h 中
    printk("led open!\r\n");
    return 0;
}

int led_release (struct inode *i, struct file *f)
{
    printk("led release!\r\n");
    return 0;
}

// ssize_t 定义在 linux/types.h 中
// __user 定义在 linux/compiler.h 中
// size_t 定义在 linux/types.h 中
// loff_t 定义在 linux/types.h 中
ssize_t led_read (struct file *f, char __user *b, size_t c, loff_t * l)
{
    printk("led read!\r\n");
    return 0;
}

ssize_t led_write (struct file *f, const char __user *b, size_t c, loff_t *l)
{
    printk("led write!\r\n");
    return 0;
}

// 声明在 linux/fs.h 头文件中
static struct file_operations fops = {
    .open = led_open,
    .release = led_release,
    .read = led_read,
    .write = led_write,
};

/* 驱动入口函数 */
static int __init led_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;
    // 声明在 linux/fs.h 头文件中
    retvalue = register_chrdev(200,"chrdev",&fops);
    if(retvalue < 0){
        /* 字符设备注册失败 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit led_exit(void)
{
    /* 出口函数具体内容 */
    // 声明在 linux/fs.h 头文件中
    unregister_chrdev(200,"chrdev");
}

// 声明在 linux/init.h 头文件中
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(led_init);
module_exit(led_exit);

// 声明在 linux/module.h 头文件中
MODULE_LICENSE("GPL");

2、makefile

KERNELDIR := /home/onlylove/linux/linux/lq_linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := led.o

build: kernel_modules

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

3、LED 初始化

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;

以上通过 define 定义物理地址,通过变量保存虚拟地址。物理地址和虚拟地址映射通过 open 函数完成,代码如下:

int led_open (struct inode *i, struct file *f)
{
    // printk 声明在 linux/printk.h 中
    printk("led open!\r\n");
    /* 寄存器地址映射 */
    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);

    return 0;
}

取消地址映射通过 close 函数完成,代码如下:

int led_release (struct inode *i, struct file *f)
{
    printk("led release!\r\n");
    /* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

    return 0;
}

2、初始化

初始化包括配置时钟、引脚复用、引脚电平。

初始化代码在 open 函数中完成,添加初始化程序后代码如下:

int led_open (struct inode *i, struct file *f)
{
    u32 val = 0;

    // printk 声明在 linux/printk.h 中
    printk("led open!\r\n");
    /* 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的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	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);

    return 0;
}

3、控制 LED 灭亮

控制灯的灭亮,由 write 函数完成。

1、从用户空间拷贝数据

extern inline long
copy_from_user(void *to, const void __user *from, long n)
{
	return __copy_tofrom_user(to, (__force void *)from, n, from);
}

/*
 * to:内核空间数据存储缓存
 * from:指向用户空间数据缓存区
 * n:拷贝数据长度
 */

2、LED 亮灭控制

#define LEDOFF 	0				/* 关灯 */
#define LEDON 	1				/* 开灯 */

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
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);
	}	
}

3、write 代码

/*
 * @description	: 向设备写数据 
 * @param - f 	: 设备文件,表示打开的文件描述符
 * @param - b 	: 要写给设备写入的数据
 * @param - c 	: 要写入的数据长度
 * @param - l 	: 相对于文件首地址的偏移
 * @return 		: 写入的字节数,如果为负值,表示写入失败
 */
ssize_t led_write (struct file *f, const char __user *b, size_t c, loff_t *l)
{
    int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

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

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	
    return 0;
}

4、驱动代码

#include "linux/init.h"
#include "linux/module.h"
#include "linux/fs.h"
#include "linux/types.h"
#include "asm/io.h"
#include "asm/uaccess.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;

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
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);
	}	
}

// struct inode 声明在 linux/fs.h 中
// struct file 声明在 linux/fs.h 中
int led_open (struct inode *i, struct file *f)
{
    u32 val = 0;

    // printk 声明在 linux/printk.h 中
    printk("led open!\r\n");
    /* 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的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	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);

    return 0;
}

int led_release (struct inode *i, struct file *f)
{
    printk("led release!\r\n");
    /* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

    return 0;
}

// ssize_t 定义在 linux/types.h 中
// __user 定义在 linux/compiler.h 中
// size_t 定义在 linux/types.h 中
// loff_t 定义在 linux/types.h 中
ssize_t led_read (struct file *f, char __user *b, size_t c, loff_t * l)
{
    printk("led read!\r\n");
    return 0;
}

/*
 * @description	: 向设备写数据 
 * @param - f 	: 设备文件,表示打开的文件描述符
 * @param - b 	: 要写给设备写入的数据
 * @param - c 	: 要写入的数据长度
 * @param - l 	: 相对于文件首地址的偏移
 * @return 		: 写入的字节数,如果为负值,表示写入失败
 */
ssize_t led_write (struct file *f, const char __user *b, size_t c, loff_t *l)
{
    int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

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

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	
    return 0;
}

// 声明在 linux/fs.h 头文件中
static struct file_operations fops = {
    .open = led_open,
    .release = led_release,
    .read = led_read,
    .write = led_write,
};

/* 驱动入口函数 */
static int __init led_init(void)
{
    /* 入口函数具体内容 */
    int retvalue = 0;
    // 声明在 linux/fs.h 头文件中
    retvalue = register_chrdev(LED_MAJOR,LED_NAME,&fops);
    if(retvalue < 0){
        /* 字符设备注册失败 */
    }
    return 0;
}

/* 驱动出口函数 */
static void __exit led_exit(void)
{
    /* 出口函数具体内容 */
    // 声明在 linux/fs.h 头文件中
    unregister_chrdev(LED_MAJOR,LED_NAME);
}

// 声明在 linux/init.h 头文件中
/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(led_init);
module_exit(led_exit);

// 声明在 linux/module.h 头文件中
MODULE_LICENSE("GPL");

5、应用程序代码

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "stdio.h"

int main(int argc, char *argv[])
{
    int fd = 0, retvalue = 0;
    char writebuf[1] = "";
    fd = open(argv[1],O_RDWR);
    if(fd < 0){
        printf("Can't open file %s\r\n", argv[1]);
        return -1;
    }
     writebuf[0] = atoi(argv[2]);
    // 打开 led
    write(fd, writebuf, 1);
    retvalue = close(fd);
    if(retvalue < 0){
        printf("Can't close file %s\r\n", argv[1]);
        return -1;
    }

    return 0;
}

编译应用程序指令如下:

arm-linux-gnueabihf-gcc led_app.c -o led_app

6、验证

1、加载驱动

加载驱动

/ # ls
bin      etc      led_app  linuxrc  proc     sbin     tmp
dev      led.ko   lib      mnt      root     sys      usr
/ # insmod led.ko
/ #
/ # lsmod
Module                  Size  Used by    Tainted: G
led                     2274  0

加载驱动也可以使用 modprobe 命令。

查看驱动加载情况

/ # cat /proc/devices
Character devices:
  1 mem
  4 /dev/vc/0
  4 tty
  5 /dev/tty
  5 /dev/console
  5 /dev/ptmx
  7 vcs
 10 misc
 13 input
 29 fb
 81 video4linux
 89 i2c
 90 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
200 led
207 ttymxc
216 rfcomm
226 drm
249 ttyLP
250 iio
251 watchdog
252 ptp
253 pps
254 rtc

Block devices:
  1 ramdisk
259 blkext
  7 loop
  8 sd
 31 mtdblock
 65 sd
 66 sd
 67 sd
 68 sd
 69 sd
 70 sd
 71 sd
128 sd
129 sd
130 sd
131 sd
132 sd
133 sd
134 sd
135 sd
179 mmc
/ #

2、创建设备文件

命令如下所示:

mknod /dev/led c 200 0

查看创建结果:

/ # ls /dev/
autofs              ram11               tty36
bus                 ram12               tty37
console             ram13               tty38
cpu_dma_latency     ram14               tty39
dri                 ram15               tty4
fb0                 ram2                tty40
full                ram3                tty41
fuse                ram4                tty42
hwrng               ram5                tty43
i2c-0               ram6                tty44
i2c-1               ram7                tty45
input               ram8                tty46
kmsg                ram9                tty47
led                 random              tty48
loop-control        rtc0                tty49
loop0               snd                 tty5
loop1               tty                 tty50
loop2               tty0                tty51
loop3               tty1                tty52
loop4               tty10               tty53
loop5               tty11               tty54
loop6               tty12               tty55
loop7               tty13               tty56
mem                 tty14               tty57
memory_bandwidth    tty15               tty58
mmcblk0             tty16               tty59
mmcblk0p1           tty17               tty6
mmcblk1             tty18               tty60
mmcblk1boot0        tty19               tty61
mmcblk1boot1        tty2                tty62
mmcblk1p1           tty20               tty63
mmcblk1p2           tty21               tty7
mmcblk1rpmb         tty22               tty8
mxc_asrc            tty23               tty9
network_latency     tty24               ttymxc0
network_throughput  tty25               ttymxc1
null                tty26               ubi_ctrl
pps0                tty27               urandom
pps1                tty28               vcs
ptmx                tty29               vcs1
ptp0                tty3                vcsa
ptp1                tty30               vcsa1
pts                 tty31               video0
pxp_device          tty32               watchdog
ram0                tty33               watchdog0
ram1                tty34               zero
ram10               tty35               ⚌A-⚌
/ #

通过以上日志可以确定驱动文件加载成功。

3、运行应用程序

点亮 LED,指令如下:

./led_app /dev/led 1

熄灭 LED ,指令如下:

./led_app /dev/led 1

4、测试

执行 app 程序日志如下:

/ # ./led_app /dev/led 1
led open!
led write!
led release!
/ # ./led_app /dev/led 0
led open!
led write!
led release!
/ #

通过观察开发板上 LED 灯状态,可以确定 LED 可以正常打开和关闭。

上一篇:.NET项目部署到Docker容器


下一篇:PHP生成一个六位数的邀请码