linux驱动移植-LED字符设备驱动
我们在linux驱动基础概念这一节中粗略介绍了linux驱动的概念,以及应用程序是如何调用驱动程序的。
这一节我们将一点亮LED为例来介绍字符设备驱动的编写。
一、LED硬件资源
1.1 硬件接线
查看Mini2440原理图、S3C2440数据手册,了解如何点亮LED。在Mini2440裸机开发之点亮LED中我们已经介绍了Mini2440 LED1~LED4的接线方式,以及寄存器的设置,这里简单说一下,就不具体介绍了:
- LED1~LED4对应引脚GPB5~GPB8,以点亮LED1为例;
- 配置控制寄存器GPBCON(0x56000010)的bit[11:10]=01,使GPB5引脚为输出模式;
- 配置数据寄存器GPBDAT(0x56000014)的bit5=0,使GPB5引脚输出低电平;
二、LED驱动程序
在/work/sambashare/drivers下创建2.led_dev文件夹。用来保存LED驱动程序以及测试应用程序。
2.1 编写led_open、led_write函数
/* GPB寄存器 */ static volatile unsigned long *gpbcon = NULL; static volatile unsigned long *gpbdata = NULL; /* GPB5~GPB8配置为输出 */ static int led_open(struct inode *inode, struct file *file) { *gpbcon &= ~((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16)); *gpbcon |= ((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16)); return 0; } /* 点亮/熄灭 LED01~LED4 */ static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { int val; copy_from_user(&val, buf, count); // 用户空间到内核空间传递数据 printk("value %d",val); if(val == 1){ /* 点亮 */ *gpbdata &= ~((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8)); } else{ /* 熄灭 */ *gpbdata |= ((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8)); } return 0; }
2.2 注册LED驱动程序
static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .write = led_write, }; static dev_t devid; // 起始设备编号 static struct cdev led_cdev; // 保存操作结构体的字符设备 static struct class *led_cls; static struct device *led_dev; static int led_init(void) { int ret = 0; printk("%s enter.\n", __func__); /* 动态分配字符设备: (major,0) */ ret = alloc_chrdev_region(&devid, 0, 1,"led"); // ls /proc/devices看到的名字 /* 返回值为负数,表示操作失败 */ if(ret < 0){ printk("alloc char dev region error\n"); goto fail_devid; } /* 初始化字符设备,添加字符设备 */ cdev_init(&led_cdev, &led_fops); ret = cdev_add(&led_cdev, devid, 1); /* 返回值为负数,表示操作失败 */ if (ret < 0) { printk("char device add failed\n"); goto fail_cdev; }else{ printk("char device add success\n"); } /* 创建类,它会在sys目录下创建/sys/class/led这个类 */ led_cls = class_create(THIS_MODULE, "led"); if(IS_ERR(led_cls)){ printk("create class failed\n"); ret = PTR_ERR(led_cls); goto fail_class; }else{ printk("create class success\n"); } /* 在/sys/class/led下创建led0设备,然后mdev通过这个自动创建/dev/led0这个设备节点 */ led_dev = device_create(led_cls, NULL, devid, NULL, "led0"); if(IS_ERR(led_dev)){ printk("create device failed\n"); ret = PTR_ERR(led_dev); goto fail_device; }else{ printk("create device success\n"); } gpbcon = (volatile unsigned long *)ioremap(0x56000010, 16); gpbdata = (volatile unsigned long *)ioremap(0x56000014, 16); return 0; fail_device: class_destroy(led_cls); fail_class: cdev_del(&led_cdev); fail_cdev: unregister_chrdev_region(devid, 1); fail_devid: return ret; }
几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,CPU对IO端口的编址方式有两种:
- I/O 映射方式(I/O-mapped):典型地,如X86处理器为外设专门实现了一个单独的地址空间,称为"I/O地址空间"或者"I/O端口空间",CPU通过专门的I/O指令(如X86的IN和OUT指令)来访问这一空间中的地址单元。
- 内存映射方式(Memory-mapped):RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以像访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。
但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。
一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。
Linux在io.h头文件中声明了函数ioremap,用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中(这里是内核空间):
#define ioremap(cookie,size) __ioremap(cookie,size,0) void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);
其中:
- phys_addr:要映射的起始的IO地址;
- size:要映射的空间的大小;
- flags:要映射的IO空间和权限有关的标志;
该函数返回映射后的内核虚拟地址(3G-4G). 接着便可以通过读写该返回的内核虚拟地址去访问之这段I/O内存资源。在本例中就是通过读写ioremap之后的虚拟地址进行控制io引脚的。
2.3 卸载LED驱动程序
static void __exit led_exit(void) { /* 注销虚拟地址 */ iounmap(gpbcon); iounmap(gpbdata); printk("led driver exit\n"); /* 注销类、以及类设备 /sys/class/led会被移除*/ device_destroy(led_cls, devid); class_destroy(led_cls); cdev_del(&led_cdev); unregister_chrdev_region(devid, 1); return; }
使用iounmap取消ioremap所做的映射。
2.4 led_dev.c完整代码
#include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/io.h> #include <linux/uaccess.h> /* GPB寄存器 */ static volatile unsigned long *gpbcon = NULL; static volatile unsigned long *gpbdata = NULL; /* GPB5~GPB8配置为输出 */ static int led_open(struct inode *inode, struct file *file) { *gpbcon &= ~((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16)); *gpbcon |= ((0x01 << 10) | (0x01 << 12) | (0x01 << 14) | (0x01 << 16)); return 0; } /* 点亮/熄灭 LED01~LED4 */ static ssize_t led_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { int val; copy_from_user(&val, buf, count); // 用户空间到内核空间传递数据 printk("value %d",val); if(val == 1){ /* 点亮 */ *gpbdata &= ~((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8)); } else{ /* 熄灭 */ *gpbdata |= ((0x01 << 5) | (0x01 << 6) | (0x01 << 7) | (0x01 << 8)); } return 0; } static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .write = led_write, }; static dev_t devid; // 起始设备编号 static struct cdev led_cdev; // 保存操作结构体的字符设备 static struct class *led_cls; static struct device *led_dev; static int led_init(void) { int ret = 0; printk("%s enter.\n", __func__); /* 动态分配字符设备: (major,0) */ ret = alloc_chrdev_region(&devid, 0, 1,"led"); // ls /proc/devices看到的名字 /* 返回值为负数,表示操作失败 */ if(ret < 0){ printk("alloc char dev region error\n"); goto fail_devid; } /* 初始化字符设备,添加字符设备 */ cdev_init(&led_cdev, &led_fops); ret = cdev_add(&led_cdev, devid, 1); /* 返回值为负数,表示操作失败 */ if (ret < 0) { printk("char device add failed\n"); goto fail_cdev; }else{ printk("char device add success\n"); } /* 创建类,它会在sys目录下创建/sys/class/led这个类 */ led_cls = class_create(THIS_MODULE, "led"); if(IS_ERR(led_cls)){ printk("create class failed\n"); ret = PTR_ERR(led_cls); goto fail_class; }else{ printk("create class success\n"); } /* 在/sys/class/led下创建led0设备,然后mdev通过这个自动创建/dev/led0这个设备节点 */ led_dev = device_create(led_cls, NULL, devid, NULL, "led0"); if(IS_ERR(led_dev)){ printk("create device failed\n"); ret = PTR_ERR(led_dev); goto fail_device; }else{ printk("create device success\n"); } gpbcon = (volatile unsigned long *)ioremap(0x56000010, 16); gpbdata = (volatile unsigned long *)ioremap(0x56000014, 16); return 0; fail_device: class_destroy(led_cls); fail_class: cdev_del(&led_cdev); fail_cdev: unregister_chrdev_region(devid, 1); fail_devid: return ret; } static void __exit led_exit(void) { /* 注销虚拟地址 */ iounmap(gpbcon); iounmap(gpbdata); printk("led driver exit\n"); /* 注销类、以及类设备 /sys/class/led会被移除*/ device_destroy(led_cls, devid); class_destroy(led_cls); cdev_del(&led_cdev); unregister_chrdev_region(devid, 1); return; } module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
2.5 Makefile
KERN_DIR :=/work/sambashare/linux-5.2.8 all: make -C $(KERN_DIR) M=`pwd` modules clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order obj-m += led_dev.o
三、LED驱动测试应用程序
在2.led_dev下创建test文件夹,保存测试应用程序。
3.1 main.c
#include <sys/stat.h> #include <fcntl.h> #include <stdio.h> void print_usage(char *file) { printf("Usage:\n"); printf("%s <dev> <on|off>\n",file); printf("eg. \n"); printf("%s /dev/led0 on\n", file); printf("%s /dev/led0 off\n", file); } int main(int argc,char **argv) { int fd; int val; char *filename; if (argc != 3){ print_usage(argv[0]); return 0; } filename = argv[1]; fd = open(filename,O_RDWR); if(fd == -1){ printf("can't open %s!\n",filename); return 0; } if (!strcmp("on", argv[2])){ // 亮灯 val = 1; printf("%s on!\n",filename); write(fd, &val, 4); }else if (!strcmp("off", argv[2])){ // 灭灯 val = 0; printf("%s off!\n",filename); write(fd, &val, 4); }else{ print_usage(argv[0]); } return 0; }
3.2 Makefile
all: arm-linux-gcc -march=armv4t -o main main.c clean: rm -rf *.o main
四、烧录开发板测试
LED驱动目录结构如下:
4.1 编译驱动
执行make命令编译驱动,并将驱动程序拷贝到nfs文件系统:
cd /work/sambashare/drivers/2.led_dev make cp /work/sambashare/drivers/2.led_dev/led_dev.ko /work/nfs_root/rootfs
安装驱动:
[root@zy:/]# insmod led_dev.ko led_dev: loading out-of-tree module taints kernel. register_chrdev_region ok
查看设备节点文件:
[root@zy:/]# ls /dev/led0 -l crw-rw---- 1 0 0 249, 0 Jan 1 00:00 /dev/led0
4.2 编译测试应用程序
执行make命令编译测试应用程序,并将测试应用程序拷贝到nfs文件系统:
cd test make cp ./main /work/nfs_root/rootfs
运行应用程序:
./main /dev/led0 on
./main /dev/led0 off
可以看到LED1~LED4同时点亮和同时熄灭。
如果你想单独控制每一个LED,那需要为每个LED编写对应的驱动程序,这里就不演示了。
4.3 卸载LED驱动
通过用lsmod可以查看当前安装了哪些驱动:
[root@zy:/]# lsmod led_dev 1956 0 - Live 0xbf004000 (O)
卸载时直接运行:
rmmod led_dev
五、代码下载
Young / s3c2440_project[drivers]
参考文章