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