Linux
下的任何外设驱动,最终都是要配置相应的硬件寄存器。所以本章的 LED
灯驱动最终也是对 I.MX6ULL
的 IO
口进行配置,与裸机实验不同的是,在 Linux
下编写驱动要符合 Linux
的驱动框架。
Linux
内核启动的时候会初始化 MMU
,设置好内存映射,设置好以后 CPU
访问的都是虚拟地址。
在编写驱动之前,我们需要先简单了解一下 MMU
这个神器,MMU
全称叫做 Memory Manage Unit
,也就是内存管理单元。在老版本的 Linux
中要求处理器必须有 MMU
,但是现在 Linux
内核已经支持无MMU的处理器了。MMU
主要完成的功能如下:
1、完成虚拟空间到物理空间的映射。
2、内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap
和 iounmap
。
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)); }
卸载驱动的时候需要使用 iounmap
函数释放掉 ioremap
函数所做的映射,iounmap
函数原型如下:
void iounmap (volatile void __iomem *addr)
这里说的 I/O
是输入/输出的意思。这里涉及到两个概念:I/O
端口和 I/O
内存。
当外部寄存器或内存映射到 IO
空间时,称为 I/O
端口。
当外部寄存器或内存映射到内存空间时,称为 I/O
内存。
但是对于 ARM
来说没有 I/O
空间这个概念,因此 ARM
体系下只有 I/O
内存(可以直接理解为内存)。使用 ioremap
函数将寄存器的物 理地址映射到虚拟地址以后,我们就可以直接通过指针访问这些地址,但是 Linux
内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
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
就是要读取写内存地址,返回值就是读取到的数据。
void writeb(u8 value, volatile void __iomem *addr) void writew(u16 value, volatile void __iomem *addr) void writel(u32 value, volatile void __iomem *addr)
writeb
、writew
和 writel
这三个函数分别对应 8bit
、16bit
和 32bit
写操作,参数 value
是要写入的数值,addr
是要写入的地址。
https://blog.csdn.net/OnlyLove_/article/details/121757151
创建工程文件 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 }
#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");
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
/* 寄存器物理地址 */ #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; }
初始化包括配置时钟、引脚复用、引脚电平。
初始化代码在 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; }
控制灯的灭亮,由 write
函数完成。
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:拷贝数据长度 */
#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); } }
/* * @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; }
#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");
#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
加载驱动:
/ # 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 / #
命令如下所示:
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-⚌ / #
通过以上日志可以确定驱动文件加载成功。
点亮 LED
,指令如下:
./led_app /dev/led 1
熄灭 LED
,指令如下:
./led_app /dev/led 1
执行 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
可以正常打开和关闭。