嵌入式Linux中的LED驱动控制(使用多个次设备号)
在前面的LED驱动控制中,都只使用了一个设备节点(一个次设备号)来进行操作,本例来讨论一下如何把三个基色的LED分别当成三个次设备,即产生出三个设备节点文件,但共用一个设备驱动(同一个主设备号),应用程序各自控制各自的LED 。
下面先给出完整的驱动程序代码,文件名仍为led.c。
#include <linux/init.h> #include <linux/module.h> #include <linux/cdev.h> #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/io.h> //以下定义总线及寄存器的物理地址 #define AHB4_PERIPH_BASE (0x50000000) #define RCC_BASE (AHB4_PERIPH_BASE + 0x0000) #define RCC_MP_GPIOENA (RCC_BASE + 0xA28) #define GPIOA_BASE (AHB4_PERIPH_BASE + 0x2000) #define GPIOA_MODER (GPIOA_BASE + 0x0000) #define GPIOA_OTYPER (GPIOA_BASE + 0x0004) #define GPIOA_OSPEEDR (GPIOA_BASE + 0x0008) #define GPIOA_PUPDR (GPIOA_BASE + 0x000C) #define GPIOA_ODR (GPIOA_BASE + 0x0014) #define GPIOA_BSRR (GPIOA_BASE + 0x0018) #define GPIOG_BASE (AHB4_PERIPH_BASE + 0x8000) #define GPIOG_MODER (GPIOG_BASE + 0x0000) #define GPIOG_OTYPER (GPIOG_BASE + 0x0004) #define GPIOG_OSPEEDR (GPIOG_BASE + 0x0008) #define GPIOG_PUPDR (GPIOG_BASE + 0x000C) #define GPIOG_ODR (GPIOG_BASE + 0x0014) #define GPIOG_BSRR (GPIOG_BASE + 0x0018) #define GPIOB_BASE (AHB4_PERIPH_BASE + 0x3000) #define GPIOB_MODER (GPIOB_BASE + 0x0000) #define GPIOB_OTYPER (GPIOB_BASE + 0x0004) #define GPIOB_OSPEEDR (GPIOB_BASE + 0x0008) #define GPIOB_PUPDR (GPIOB_BASE + 0x000C) #define GPIOB_ODR (GPIOB_BASE + 0x0014) #define GPIOB_BSRR (GPIOB_BASE + 0x0018) //以下定义时钟控制寄存器名称 volatile void __iomem *RCC_MP_AHB4ENSETR; volatile void __iomem *GPIO_MODER_PA; volatile void __iomem *GPIO_OTYPER_PA; volatile void __iomem *GPIO_OSPEEDR_PA; volatile void __iomem *GPIO_PUPDR_PA; volatile void __iomem *GPIO_ODR_PA; volatile void __iomem *GPIO_BSRR_PA; volatile void __iomem *GPIO_MODER_PB; volatile void __iomem *GPIO_OTYPER_PB; volatile void __iomem *GPIO_OSPEEDR_PB; volatile void __iomem *GPIO_PUPDR_PB; volatile void __iomem *GPIO_ODR_PB; volatile void __iomem *GPIO_BSRR_PB; volatile void __iomem *GPIO_MODER_PG; volatile void __iomem *GPIO_OTYPER_PG; volatile void __iomem *GPIO_OSPEEDR_PG; volatile void __iomem *GPIO_PUPDR_PG; volatile void __iomem *GPIO_ODR_PG; volatile void __iomem *GPIO_BSRR_PG; dev_t devid; //设备号 struct cdev chrdev[3]; //字符设备结构体数组 struct class *class; //类结构体 struct device *device; //设备结构体 //实现open函数,为file_oprations结构体成员函数 static int led_open(struct inode *inode, struct file *filp) { unsigned int tmp; //以下使能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(RCC_MP_AHB4ENSETR); tmp |= (0x1 << 6) | (0x1 << 1) | (0x1 << 0); iowrite32(tmp, RCC_MP_AHB4ENSETR); return 0; } //实现write函数,为file_oprations结构体成员函数 static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { unsigned char value; unsigned long n; unsigned int minor; minor = iminor(file_inode(filp)); //获取次设备号 n = copy_from_user(&value, buf, cnt); //从应用空间获取值 switch(minor) { case 0: //次设备号0表示红色LED if(value) iowrite32(0x2000, GPIO_BSRR_PA);//熄灭红色LED else iowrite32(0x20000000, GPIO_BSRR_PA);//点亮红色LED break; case 1: //次设备号1表示绿色LED if(value) iowrite32(0x04, GPIO_BSRR_PG);//熄灭绿色LED else iowrite32(0x40000, GPIO_BSRR_PG);//点亮绿色LED break; case 2: //次设备号2表示蓝色LED if(value) iowrite32(0x20, GPIO_BSRR_PB);//熄灭蓝色LED else iowrite32(0x200000, GPIO_BSRR_PB);//点亮蓝色LED default: break; } return cnt; } //实现release函数,为file_oprations结构体函数 static int led_release(struct inode *inode, struct file *filp) { unsigned int tmp; //以下禁能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(RCC_MP_AHB4ENSETR); tmp &= ~0x43; iowrite32(tmp, RCC_MP_AHB4ENSETR); return 0; } //填充一个file_oprations类型的结构体,名为led_dev_fops,包含上述声明的成员函数 static struct file_operations led_dev_fops = { .owner = THIS_MODULE, .open = led_open, //指定open函数成员 .write = led_write, //指定write函数成员 .release = led_release, //指定release函数成员 }; //初始化函数,此处为驱动模块的入口函数 static int __init led_init(void) { unsigned int tmp; dev_t cur_id; //以下实现各个寄存器的地址映射 RCC_MP_AHB4ENSETR = ioremap(RCC_MP_GPIOENA, 4); GPIO_MODER_PA = ioremap(GPIOA_MODER, 4); GPIO_OTYPER_PA = ioremap(GPIOA_OTYPER, 4); GPIO_OSPEEDR_PA = ioremap(GPIOA_OSPEEDR, 4); GPIO_PUPDR_PA = ioremap(GPIOA_PUPDR, 4); GPIO_ODR_PA = ioremap(GPIOA_ODR, 4); GPIO_BSRR_PA = ioremap(GPIOA_BSRR, 4); GPIO_MODER_PB = ioremap(GPIOB_MODER, 4); GPIO_OTYPER_PB = ioremap(GPIOB_OTYPER, 4); GPIO_OSPEEDR_PB = ioremap(GPIOB_OSPEEDR, 4); GPIO_PUPDR_PB = ioremap(GPIOB_PUPDR, 4); GPIO_ODR_PB = ioremap(GPIOB_ODR, 4); GPIO_BSRR_PB = ioremap(GPIOB_BSRR, 4); GPIO_MODER_PG = ioremap(GPIOG_MODER, 4); GPIO_OTYPER_PG = ioremap(GPIOG_OTYPER, 4); GPIO_OSPEEDR_PG = ioremap(GPIOG_OSPEEDR, 4); GPIO_PUPDR_PG = ioremap(GPIOG_PUPDR, 4); GPIO_ODR_PG = ioremap(GPIOG_ODR, 4); GPIO_BSRR_PG = ioremap(GPIOG_BSRR, 4); //以下使能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(RCC_MP_AHB4ENSETR); tmp |= (0x1 << 6) | (0x1 << 1) | (0x1 << 0); iowrite32(tmp, RCC_MP_AHB4ENSETR); //以下把GPIOA、GPIOB、GPIOG端口配置为输出、上位模式 tmp = ioread32(GPIO_MODER_PA); tmp &= ~(0x3 << 26); tmp |= (0x1 << 26); iowrite32(tmp, GPIO_MODER_PA); tmp = ioread32(GPIO_MODER_PB); tmp &= ~(0x3 << 10); tmp |= (0x1 << 10); iowrite32(tmp, GPIO_MODER_PB); tmp = ioread32(GPIO_MODER_PG); tmp &= ~(0x3 << 4); tmp |= (0x1 << 4); iowrite32(tmp, GPIO_MODER_PG); tmp = ioread32(GPIO_PUPDR_PA); tmp &= ~(0x3 << 26); tmp |= (0x1 << 26); iowrite32(tmp, GPIO_PUPDR_PA); tmp = ioread32(GPIO_PUPDR_PB); tmp &= ~(0x3 << 10); tmp |= (0x1 << 10); iowrite32(tmp, GPIO_PUPDR_PB); tmp = ioread32(GPIO_PUPDR_PG); tmp &= ~(0x3 << 4); tmp |= (0x1 << 4); iowrite32(tmp, GPIO_PUPDR_PG); //以下设定GPIOA、GPIOB、GPIOG端口初始值 iowrite32(0x2000, GPIO_BSRR_PA); iowrite32(0x20, GPIO_BSRR_PB); iowrite32(0x04, GPIO_BSRR_PG); //以下禁能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(RCC_MP_AHB4ENSETR); tmp &= ~0x43; iowrite32(tmp, RCC_MP_AHB4ENSETR); //申请主设备号 if(alloc_chrdev_region(&devid, 0, 1, "led") < 0) { printk("Couldn't alloc_chrdev_region!\r\n"); return -EFAULT; } //创建类 class = class_create(THIS_MODULE, "led_dev"); //以下实现三个字符型设备的申请和注册及创建三个设备节点 for (tmp=0; tmp < 3; tmp++) { chrdev[tmp].owner = THIS_MODULE; //绑定前面声明的file_oprations类型的结构体到字符设备 cdev_init(&chrdev[tmp], &led_dev_fops); cur_id = MKDEV(MAJOR(devid), MINOR(devid) + tmp); //填充上面申请到的主设备号到字符设备 if(cdev_add(&chrdev[tmp],cur_id, 1) < 0) { printk("Couldn't add chrdev!\r\n"); return -EFAULT; } //根据创建的类生成一个设备节点 device = device_create(class, NULL, cur_id, NULL, "led%d", tmp); } return 0; } //退出函数,此处为驱动模块的出口函数 static void __exit led_exit(void) { unsigned int tmp; dev_t cur_id; //以下实现各个寄存器的解除映射 iounmap(RCC_MP_AHB4ENSETR); iounmap(GPIO_MODER_PA); iounmap(GPIO_OTYPER_PA); iounmap(GPIO_OSPEEDR_PA); iounmap(GPIO_PUPDR_PA); iounmap(GPIO_ODR_PA); iounmap(GPIO_BSRR_PA); iounmap(GPIO_MODER_PB); iounmap(GPIO_OTYPER_PB); iounmap(GPIO_OSPEEDR_PB); iounmap(GPIO_PUPDR_PB); iounmap(GPIO_ODR_PB); iounmap(GPIO_BSRR_PB); iounmap(GPIO_MODER_PG); iounmap(GPIO_OTYPER_PG); iounmap(GPIO_OSPEEDR_PG); iounmap(GPIO_PUPDR_PG); iounmap(GPIO_ODR_PG); iounmap(GPIO_BSRR_PG); //以下销毁三个字符型设备及三个设备节点 for (tmp=0; tmp < 3; tmp++) { //删除字符设备 cdev_del(&chrdev[tmp]); cur_id = MKDEV(MAJOR(devid), MINOR(devid) + tmp); //销毁设备节点 device_destroy(class, cur_id); } //释放主设备号 unregister_chrdev_region(devid, 1); //销毁类 class_destroy(class); } module_init(led_init); //模块入口声明 module_exit(led_exit); //模块出口声明 MODULE_LICENSE("GPL"); //GPL协议声明
接下来是配套的Makefile文件,内容如下。
KERNEL_DIR=/opt/ebf_linux_kernel_mp157_depth1/build_image/build ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- export ARCH CROSS_COMPILE obj-m := led.o all: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules clean: $(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
然后给出应用程序,内容如下,文件名为app.c。
#include <stdio.h> #include <fcntl.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) { int fd; unsigned char val = 0; if( argc != 3 ) //命令参数不对时提示 { printf("Usage :\n"); printf("%s <all|red|green|blue> <on|off>\n", argv[0]); return 0; } if(strcmp(argv[1], "all") == 0) { if(strcmp(argv[2], "on") == 0) { val = 0; //值为0时点亮 fd = open("/dev/led0", O_RDWR);//打开设备节点0 write(fd, &val, 1); //把值写入设备节点0 close(fd); //关闭设备节点0 fd = open("/dev/led1", O_RDWR);//打开设备节点1 write(fd, &val, 1); //把值写入设备节点1 close(fd); //关闭设备节点1 fd = open("/dev/led2", O_RDWR);//打开设备节点2 write(fd, &val, 1); //把值写入设备节点2 close(fd); //关闭设备节点2 } else { val = 1; //值为1时熄灭 fd = open("/dev/led0", O_RDWR);//打开设备节点0 write(fd, &val, 1); //把值写入设备节点0 close(fd); //关闭设备节点0 fd = open("/dev/led1", O_RDWR);//打开设备节点1 write(fd, &val, 1); //把值写入设备节点1 close(fd); //关闭设备节点1 fd = open("/dev/led2", O_RDWR);//打开设备节点2 write(fd, &val, 1); //把值写入设备节点2 close(fd); //关闭设备节点2 } } else { if(strcmp(argv[1], "red") == 0) { fd = open("/dev/led0", O_RDWR); //打开设备节点0 if( fd < 0 ) printf("can`t open\n"); if(strcmp(argv[2], "on") == 0) val = 0; //值为0时红色点亮 else val = 1; //值为1时红色熄灭 } else if(strcmp(argv[1], "green") == 0) { fd = open("/dev/led1", O_RDWR); //打开设备节点1 if( fd < 0 ) printf("can`t open\n"); if(strcmp(argv[2], "on") == 0) val = 0; //值为0时绿色点亮 else val = 1; //值为1时绿色熄灭 } else if(strcmp(argv[1], "blue") == 0) { fd = open("/dev/led2", O_RDWR); //打开设备节点2 if( fd < 0 ) printf("can`t open\n"); if(strcmp(argv[2], "on") == 0) val = 0; //值为0时蓝色点亮 else val = 1; //值为1时蓝色熄灭 } write(fd, &val, 1); //把值写入设备节点 close(fd); //关闭设备节点 } return 0; }
完成后,先执行make命令编译驱动程序,生成名为led.ko的驱动模块文件。然后对应用程序进行交叉编译,执行“arm-linux-gnueabihf-gcc app.c -o app”即可。最后,把编译生成的驱动模块文件led.ko和应用程序文件app一起拷贝到NFS共享目录下 ,并在开发板上执行“insmod led.ko”,把模块插入到内核中(可执行lsmod命令查看一下是否加载成功),之后可查看一下/dev目录,应该生成了三个设备节点文件led0、led1、led2,分别对应红、绿、蓝三个LED。此外,还可以执行“cat /proc/devices”查看一下led设备的主设备号。
以上都正常后,就可以执行应用程序来进行测试了。以控制红色为例,输入“./app red on”并回车,就可看到红色LED亮,输入“./app red off”并回车,就可看到红色LED灭,其他颜色的LED可如法进行测试。输入“./app all on”回车可实现三个LED全部点亮,输入“./app all off”回车实现三个LED全部熄灭。实验结果与“嵌入式Linux中的LED驱动控制”一文中的完全一样。
把上面的驱动代码和“嵌入式Linux中的LED驱动控制”一文中的代码进行比较可以看出,最大区别在于字符型cdev结构体一共注册了3个,设备节点也创建了3个。主设备号使用动态方式申请了1个,次设备号则指定了3个(0~2),即三个设备共用了一个驱动。
上面的代码中,最关键的是如何获取到打开文件的次设备号。本例在write接口函数中使用了函数iminor(file_inode(filp))来实现,minor函数调用了file_inode函数,file_inode的参数为file结构体指针。关于这部分的详细介绍可参考“嵌入式Linux中字符型驱动程序的基本框架”一文。其实,获取打开设备节点文件次设备的方式还有很多,比如使用函数iminor(inode)方式,或使用宏MINOR(inode->i_rdev)方式等等,返回值就是获取到的次设备号。