嵌入式Linux中的LED驱动控制(设备树方式)
Linux3.1之后的内核版本,引入了设备树的概念。同时,设备树还需要Bootloader的支持,如果使用Uboot,在1.1.3版本之后就可以支持设备树了。
设备树概念的提出其实有两方面的原因。其一当然是代码冗余,导致Linux内核臃肿不堪。在Linux内核源码中,只要是通过了基金会的认可,就可以把某个厂商的板级支持代码纳入到Linux内核源码当中。比如国内曾经风靡一时的友善之臂Mini2440开发板,从Linux-2.6.31版本开始就被Linux官方内核所支持了,至今还可以在Linux内核源码中看到它的相关文件(如:arch/arm/mach-s3c24xx/mach-mini2440.c以及arch/arm/configs/mini2440_defconfig等)。然而在Linux内核源码中,同样还包含有很多其他厂商的板级支持文件。其实这些开发板只要所选用的芯片相同,则很大一部分代码是相同的。这就造成了内核的冗余和臃肿,并有愈演愈烈的趋势(难怪Linus要发火了)。第二个原因,其实也是顺理成章的。从Linux2.6版本之后内核就引入了platform总线平台的概念,把驱动分成了设备(platform_device)和驱动(platform_driver)两个单独的文件,现在只要把设备文件从内核中提出来,单独形成一个第三方文件,不就不影响内核了吗。这样做还有一个好处,即这个提出来的设备文件,只要在驱动中统一相关接口和命名规则,该文件的大部分工作还可以由厂商来完成(或由厂商提供的工具来完成),大大提高了开发的效率和可靠性。这个被单独提出来的第三方文件,后来就演化成了现在的设备树配置文件。
下面就通过设备树方式来实现对LED的驱动。先给出设备树的配置内容,在内核源码(本例在/opt/ebf_linux_kernel_mp157_depth1/目录下)的arch/arm/boot/dts目录下找到一个名为“stm32mp157a-basic.dts”的文件,该文件就是开发板配套提供的设备树源文件。打开它,并在根节点的最后加入本例LED设备的配置内容,如下。
/ { model = "Embedfire STM32MP157 Star LubanCat Robot S1 Board"; compatible = "st,stm32mp157a-dk1", "st,stm32mp157"; aliases { ethernet0 = ðernet0; serial0 = &uart4; serial1 = &usart1; serial2 = &usart2; serial3 = &usart3; }; chosen { stdout-path = "serial0:115200n8"; }; memory@c0000000 { reg = <0xc0000000 0x40000000>; }; reserved-memory { #address-cells = <1>; #size-cells = <1>; ranges; retram: retram@0x38000000 { compatible = "shared-dma-pool"; reg = <0x38000000 0x10000>; no-map; }; mcuram: mcuram@0x30000000 { compatible = "shared-dma-pool"; reg = <0x30000000 0x40000>; no-map; }; mcuram2: mcuram2@0x10000000 { compatible = "shared-dma-pool"; reg = <0x10000000 0x40000>; no-map; }; vdev0vring0: vdev0vring0@10040000 { compatible = "shared-dma-pool"; reg = <0x10040000 0x2000>; no-map; }; vdev0vring1: vdev0vring1@10042000 { compatible = "shared-dma-pool"; reg = <0x10042000 0x2000>; no-map; }; vdev0buffer: vdev0buffer@10044000 { compatible = "shared-dma-pool"; reg = <0x10044000 0x4000>; no-map; }; gpu_reserved: gpu@d4000000 { reg = <0xd4000000 0x4000000>; no-map; }; }; sram: sram@10050000 { compatible = "mmio-sram"; reg = <0x10050000 0x10000>; #address-cells = <1>; #size-cells = <1>; ranges = <0 0x10050000 0x10000>; dma_pool: dma_pool@0 { reg = <0x0 0x10000>; pool; }; }; leds { compatible = "gpio-leds"; status = "okay"; heartbeat { label = "heartbeat"; gpios = <&gpioa 14 GPIO_ACTIVE_HIGH>; linux,default-trigger = "heartbeat"; default-state = "off"; }; }; v3v3: regulator-3p3v { compatible = "regulator-fixed"; regulator-name = "v3v3"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; regulator-always-on; regulator-boot-on; }; vdd: regulator-vdd { compatible = "regulator-fixed"; regulator-name = "vdd"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; regulator-always-on; regulator-boot-on; }; vdd_usb: regulator-vdd-usb { compatible = "regulator-fixed"; regulator-name = "vdd_usb"; regulator-min-microvolt = <3300000>; regulator-max-microvolt = <3300000>; regulator-always-on; regulator-boot-on; }; v2v8: v2v8 { compatible = "regulator-fixed"; regulator-name = "v2v8"; regulator-min-microvolt = <2800000>; regulator-max-microvolt = <2800000>; regulator-always-on; regulator-boot-on; }; vbus_otg: regulator-vbus-otg { compatible = "regulator-fixed"; regulator-name = "vbus_otg"; regulator-min-microvolt = <5000000>; regulator-max-microvolt = <5000000>; regulator-always-on; regulator-boot-on; }; usb_phy_tuning: usb-phy-tuning { st,hs-dc-level = <2>; st,fs-rftime-tuning; st,hs-rftime-reduction; st,hs-current-trim = <15>; st,hs-impedance-trim = <1>; st,squelch-level = <3>; st,hs-rx-offset = <2>; st,no-lsfs-sc; }; //以下为本次LED的追加内容 rgb_led{ #address-cells = <1>; #size-cells = <1>; compatible = "fire,rgb_led"; ranges; //红色LED节点 led_red@0x50002000{ compatible = "fire,led_red"; reg = < 0x50002000 0x00000004 0x50002004 0x00000004 0x50002008 0x00000004 0x5000200C 0x00000004 0x50002018 0x00000004 0x50000A28 0x00000004 >; status = "okay"; }; //绿色LED节点 led_green@0x50008000{ compatible = "fire,led_green"; reg = < 0x50008000 0x00000004 0x50008004 0x00000004 0x50008008 0x00000004 0x5000800C 0x00000004 0x50008018 0x00000004 >; status = "okay"; }; //蓝色LED节点 led_blue@0x50003000{ compatible = "fire,led_blue"; reg = < 0x50003000 0x00000004 0x50003004 0x00000004 0x50003008 0x00000004 0x5000300C 0x00000004 0x50003018 0x00000004 >; status = "okay"; }; }; };
注意,以上内容只是设备树文件stm32mp157a-basic.dts中的一部分内容,并未全部给出。上面内容中最末尾的部分才是本次追加的内容,其他部分内容是原设备树就有的,不要改动(包括未给出的部分),完成后保存并编译它。编译要在源码根目录下进行(即/opt/ebf_linux_kernel_mp157_depth1/目录下),先执行make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig进行配置,然后执行make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs进行编译。编译完成后,会在设备树所在目录下(arch/arm/boot/dts)生成名为stm32mp157a-basic.dtb的设备树文件,把该文件通过NFS拷贝到开发板的/boot/dts/目录下并替换原有设备树文件,然后执行reboot重启开发板(不能按reset键重启)。
以下是平台驱动部分的代码,文件名为led.c。
#include <linux/init.h> #include <linux/module.h> #include <linux/fs.h> #include <linux/cdev.h> #include <linux/uaccess.h> #include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/errno.h> #include <linux/gpio.h> #include <asm/mach/map.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_gpio.h> #include <asm/io.h> #include <linux/device.h> #include <linux/platform_device.h> static dev_t devid; //设备号 static struct cdev led_cdev; //定义字符型结构体 struct class *led_class; //类结构体 struct device_node *rgb_led_device_node; //rgb_led的设备树节点结构体 //以下定义led资源结构体,保存获取得到的节点信息以及转换后的虚拟寄存器地址 struct led_resource { struct device_node *device_node; void __iomem *MODER; void __iomem *OTYPER; void __iomem *OSPEEDR; void __iomem *PUPDR; void __iomem *BSRR; }; static void __iomem *clkaddr; //端口时钟变量 //以下定义RGB三个灯的led_resource结构体,保存获取得到的节点信息 struct led_resource led_red; struct led_resource led_green; struct led_resource led_blue; //实现open函数,为file_oprations结构体成员函数 static int led_open(struct inode *inode, struct file *filp) { unsigned int tmp; //以下使能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(clkaddr); tmp |= 0x43; iowrite32(tmp, clkaddr); 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; n = copy_from_user(&value, buf, cnt); //从应用空间获取值 switch(value) //根据应用空间的值判断具体操作 { case 0: //全部点亮三个LED iowrite32(0x20000000, led_red.BSRR); iowrite32(0x200000, led_blue.BSRR); iowrite32(0x40000, led_green.BSRR); break; case 1: //点亮红色LED iowrite32(0x20000000, led_red.BSRR); break; case 2: //点亮绿色LED iowrite32(0x40000, led_green.BSRR); break; case 3: //点亮蓝色LED iowrite32(0x200000, led_blue.BSRR); break; case 4: //熄灭红色LED iowrite32(0x2000, led_red.BSRR); break; case 5: //熄灭绿色LED iowrite32(0x04, led_green.BSRR); break; case 6: //熄灭蓝色LED iowrite32(0x20, led_blue.BSRR); break; case 7: //全部熄灭三个LED iowrite32(0x2000, led_red.BSRR); iowrite32(0x20, led_blue.BSRR); iowrite32(0x04, led_green.BSRR); break; default: //全部熄灭 iowrite32(0x2000, led_red.BSRR); iowrite32(0x20, led_blue.BSRR); iowrite32(0x04, led_green.BSRR); break; } return cnt; } //实现release函数,为file_oprations结构体函数 static int led_release(struct inode *inode, struct file *filp) { unsigned int tmp; //以下禁能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(clkaddr); tmp &= ~0x43; iowrite32(tmp, clkaddr); 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函数成员 }; //probe函数中,驱动提取设备树中的资源,并完成字符设备的注册 static int led_pdrv_probe(struct platform_device *pdv) { unsigned int tmp; //获取rgb_led的设备树节点 rgb_led_device_node = of_find_node_by_path("/rgb_led"); if (rgb_led_device_node == NULL) { printk(KERN_ERR "\t get rgb_led failed! \n"); return -1; } //获取rgb_led节点的红灯子节点 led_red.device_node = of_find_node_by_name(rgb_led_device_node,"led_red"); if (led_red.device_node == NULL) { printk(KERN_ERR "\n get rgb_led_red_device_node failed ! \n"); return -1; } //以下获取设备节点中红灯子节点的reg属性并转化为虚拟地址 led_red.MODER = of_iomap(led_red.device_node, 0); led_red.OTYPER = of_iomap(led_red.device_node, 1); led_red.OSPEEDR = of_iomap(led_red.device_node, 2); led_red.PUPDR = of_iomap(led_red.device_node, 3); led_red.BSRR = of_iomap(led_red.device_node, 4); clkaddr = of_iomap(led_red.device_node, 5); //以下使能GPIOA、GPIOB、GPIOG端口时钟 tmp = ioread32(clkaddr); tmp |= 0x43; iowrite32(tmp, clkaddr); //以下设置模式寄存器:输出模式 tmp = ioread32(led_red.MODER); tmp &= ~(0x3 << (13 * 2)); tmp |= (0x1 << (13 * 2)); iowrite32(tmp, led_red.MODER); //以下设置输出类型寄存器:推挽模式 tmp = ioread32(led_red.OTYPER); tmp &= ~(0x1 << 13); iowrite32(tmp, led_red.OTYPER); //以下设置输出速度寄存器:高速 tmp = ioread32(led_red.OSPEEDR); tmp &= ~(0x3 << (13 * 2)); tmp |= (0x2 << (13 * 2)); iowrite32(tmp, led_red.OSPEEDR); //以下设置上下拉寄存器:上拉 tmp = ioread32(led_red.PUPDR); tmp &= ~(0x3 << (13 * 2)); tmp |= (0x1 << (13 * 2)); iowrite32(tmp,led_red.PUPDR); //以下设置置位寄存器:默认输出高电平 tmp = ioread32(led_red.BSRR); tmp |= (0x1 << 13); iowrite32(tmp, led_red.BSRR); //获取rgb_led节点的绿灯子节点 led_green.device_node = of_find_node_by_name(rgb_led_device_node,"led_green"); if (led_green.device_node == NULL) { printk(KERN_ERR "\n get rgb_led_green_device_node failed ! \n"); return -1; } //以下获取设备节点中绿灯子节点的reg属性并转化为虚拟地址 led_green.MODER = of_iomap(led_green.device_node, 0); led_green.OTYPER = of_iomap(led_green.device_node, 1); led_green.OSPEEDR = of_iomap(led_green.device_node, 2); led_green.PUPDR = of_iomap(led_green.device_node, 3); led_green.BSRR = of_iomap(led_green.device_node, 4); //以下设置模式寄存器:输出模式 tmp = ioread32(led_green.MODER); tmp &= ~(0x3 << (2 * 2)); tmp |= (0x1 << (2 * 2)); iowrite32(tmp,led_green.MODER); //以下设置输出类型寄存器:推挽模式 tmp = ioread32(led_green.OTYPER); tmp &= ~(0x1 << 2); iowrite32(tmp, led_green.OTYPER); //以下设置输出速度寄存器:高速 tmp = ioread32(led_green.OSPEEDR); tmp &= ~(0x3 << (2 * 2)); tmp |= (0x2 << (2 * 2)); iowrite32(tmp, led_green.OSPEEDR); //以下设置上下拉寄存器:上拉 tmp = ioread32(led_green.PUPDR); tmp &= ~(0x3 << (2 * 2)); tmp |= (0x1 << (2 * 2)); iowrite32(tmp,led_green.PUPDR); //以下设置置位寄存器:默认输出高电平 tmp = ioread32(led_green.BSRR); tmp |= (0x1 << 2); iowrite32(tmp, led_green.BSRR); //获取rgb_led节点的蓝灯子节点 led_blue.device_node = of_find_node_by_name(rgb_led_device_node,"led_blue"); if (led_blue.device_node == NULL) { printk(KERN_ERR "\n get rgb_led_blue_device_node failed ! \n"); return -1; } //以下获取设备节点中蓝灯子节点的reg属性并转化为虚拟地址 led_blue.MODER = of_iomap(led_blue.device_node, 0); led_blue.OTYPER = of_iomap(led_blue.device_node, 1); led_blue.OSPEEDR = of_iomap(led_blue.device_node, 2); led_blue.PUPDR = of_iomap(led_blue.device_node, 3); led_blue.BSRR = of_iomap(led_blue.device_node, 4); //以下设置模式寄存器:输出模式 tmp = ioread32(led_blue.MODER); tmp &= ~(0x3 << (5 * 2)); tmp |= (0x1 << (5 * 2)); iowrite32(tmp,led_blue.MODER); //以下设置输出类型寄存器:推挽模式 tmp = ioread32(led_blue.OTYPER); tmp &= ~(0x1 << 5); iowrite32(tmp, led_blue.OTYPER); //以下设置输出速度寄存器:高速 tmp = ioread32(led_blue.OSPEEDR); tmp &= ~(0x3 << (5 * 2)); tmp |= (0x2 << (5 * 2)); iowrite32(tmp, led_blue.OSPEEDR); //以下设置上下拉寄存器:上拉 tmp = ioread32(led_blue.PUPDR); tmp &= ~(0x3 << (5 * 2)); tmp |= (0x1 << (5 * 2)); iowrite32(tmp,led_blue.PUPDR); //以下设置置位寄存器:默认输出高电平 tmp = ioread32(led_blue.BSRR); tmp |= (0x1 << 5); iowrite32(tmp, led_blue.BSRR); //申请主设备号 if (alloc_chrdev_region(&devid, 0, 1, "led") < 0) { printk("fail to alloc devid\n"); return -EFAULT; } led_cdev.owner = THIS_MODULE; //绑定前面声明的file_oprations类型的结构体到字符设备 cdev_init(&led_cdev, &led_dev_fops); //填充上面申请到的主设备号到字符设备 if ( cdev_add(&led_cdev, devid, 1) < 0) { printk("fail to add cdev\n"); return -EFAULT; } //创建一个类 led_class = class_create(THIS_MODULE, "my_leds"); //创建一个设备节点 device_create(led_class, NULL, devid, NULL, "led"); printk("platform driver probed!\n"); return 0; } //remove函数中,删除设备并释放设备号 static int led_pdrv_remove(struct platform_device *pdev) { //以下实现各个寄存器的解除映射 iounmap(clkaddr); iounmap(led_green.MODER); iounmap(led_green.OTYPER); iounmap(led_green.OSPEEDR); iounmap(led_green.PUPDR); iounmap(led_green.BSRR); iounmap(led_red.MODER); iounmap(led_red.OTYPER); iounmap(led_red.OSPEEDR); iounmap(led_red.PUPDR); iounmap(led_red.BSRR); iounmap(led_blue.MODER); iounmap(led_blue.OTYPER); iounmap(led_blue.OSPEEDR); iounmap(led_blue.PUPDR); iounmap(led_blue.BSRR); unregister_chrdev_region(devid, 1); //释放主设备号 cdev_del(&led_cdev); //删除字符设备 device_destroy(led_class, devid); //销毁设备节点 class_destroy(led_class); //销毁类 printk("platform driver removed!\n"); return 0; } //填充of_device_id结构体,名为rgb_led,用于指明匹配表 static const struct of_device_id rgb_led[] = { {.compatible = "fire,rgb_led"}, //匹配内容 {/* sentinel */} }; //以下填充一个platform_driver结构体 struct platform_driver led_platform_driver = { .probe = led_pdrv_probe, //指定probe函数成员 .remove = led_pdrv_remove, //指定remove函数成员 .driver = { .name = "rgb-leds-platform", //指定设备名称 .owner = THIS_MODULE, .of_match_table = rgb_led, //指定匹配表名称 } }; //以下定义模块的入口函数 static int __init led_pdrv_init(void) { platform_driver_register(&led_platform_driver);//注册一个platform驱动 printk("led platform driver initted!\n"); return 0; } //以下定义模块的出口函数 static void __exit led_pdrv_exit(void) { platform_driver_unregister(&led_platform_driver); //释放一个platform驱动 printk("led platform driver exited!\n"); } module_init(led_pdrv_init); module_exit(led_pdrv_exit); MODULE_LICENSE("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; fd = open("/dev/led", O_RDWR); //打开设备节点 if( fd < 0 ) printf("can`t open\n"); 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时全部点亮 else val = 7; //值为7时全部熄灭 } else if(strcmp(argv[1], "red") == 0) { if(strcmp(argv[2], "on") == 0) val = 1; //值为1时红色点亮 else val = 4; //值为4时红色熄灭 } else if(strcmp(argv[1], "green") == 0) { if(strcmp(argv[2], "on") == 0) val = 2; //值为2时绿色点亮 else val = 5; //值为5时绿色熄灭 } else if(strcmp(argv[1], "blue") == 0) { if(strcmp(argv[2], "on") == 0) val = 3; //值为3时蓝色点亮 else val = 6; //值为6时蓝色熄灭 } write(fd, &val, 1); //把值写入设备节点 close(fd); //关闭设备节点 return 0; }
完成后,先执行make命令编译驱动程序,若成功会生成名为led.ko的驱动模块文件。然后对应用程序进行交叉编译,执行“arm-linux-gnueabihf-gcc app.c -o app”即可。实验结果与“嵌入式Linux中的LED驱动控制”一文中的完全一样,这里就不给出了。
--待续--